From 867a96388230066d7ed7cdb88cd90e54f0a15670 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 27 Apr 2021 14:01:59 -0700 Subject: [PATCH] Merge from vscode bead496a613e475819f89f08e9e882b841bc1fe8 (#14883) * Merge from vscode bead496a613e475819f89f08e9e882b841bc1fe8 * Bump distro * Upgrade GCC to 4.9 due to yarn install errors * Update build image * Fix bootstrap base url * Bump distro * Fix build errors * Update source map file * Disable checkbox for blocking migration issues (#15131) * disable checkbox for blocking issues * wip * disable checkbox fixes * fix strings * Remove duplicate tsec command * Default to off for tab color if settings not present * re-skip failing tests * Fix mocha error * Bump sqlite version & fix notebooks search view * Turn off esbuild warnings * Update esbuild log level * Fix overflowactionbar tests * Fix ts-ignore in dropdown tests * cleanup/fixes * Fix hygiene * Bundle in entire zone.js module * Remove extra constructor param * bump distro for web compile break * bump distro for web compile break v2 * Undo log level change * New distro * Fix integration test scripts * remove the "no yarn.lock changes" workflow * fix scripts v2 * Update unit test scripts * Ensure ads-kerberos2 updates in .vscodeignore * Try fix unit tests * Upload crash reports * remove nogpu * always upload crashes * Use bash script * Consolidate data/ext dir names * Create in tmp directory Co-authored-by: chlafreniere Co-authored-by: Christopher Suh Co-authored-by: chgagnon --- .eslintignore | 1 + .eslintrc.json | 13 +- .github/workflows/build-chat.yml | 35 + .github/workflows/deep-classifier-runner.yml | 2 +- .github/workflows/deep-classifier-scraper.yml | 2 +- .github/workflows/latest-release-monitor.yml | 2 +- .gitignore | 22 +- .vscode/extensions.json | 3 +- .vscode/launch.json | 3 +- .vscode/notebooks/api.github-issues | 2 +- .vscode/notebooks/endgame.github-issues | 28 +- .../notebooks/grooming-delta.github-issues | 9 +- .vscode/notebooks/my-endgame.github-issues | 26 +- .vscode/notebooks/my-work.github-issues | 20 +- .vscode/notebooks/papercuts.github-issues | 44 + .vscode/notebooks/verification.github-issues | 2 +- .vscode/tasks.json | 8 +- .yarnrc | 2 +- azure-pipelines.yml | 5 + build/.moduleignore | 53 +- build/azure-pipelines/common/.gitignore | 2 - .../common/computeNodeModulesCacheKey.js | 25 + .../common/computeNodeModulesCacheKey.ts | 32 + build/azure-pipelines/common/copyArtifacts.js | 40 + build/azure-pipelines/common/createAsset.js | 94 + build/azure-pipelines/common/createAsset.ts | 3 +- build/azure-pipelines/common/createBuild.js | 51 + build/azure-pipelines/common/createBuild.ts | 3 +- .../common/extract-telemetry.sh | 4 +- .../common/installPlaywright.js | 14 + .../azure-pipelines/common/listNodeModules.js | 40 + .../azure-pipelines/common/publish-webview.js | 71 + build/azure-pipelines/common/publish.js | 224 ++ build/azure-pipelines/common/release.js | 91 + build/azure-pipelines/common/releaseBuild.js | 50 + build/azure-pipelines/common/releaseBuild.ts | 3 +- build/azure-pipelines/common/retry.js | 25 + build/azure-pipelines/common/retry.ts | 26 + build/azure-pipelines/common/sync-mooncake.js | 87 + build/azure-pipelines/common/sync-mooncake.ts | 5 +- .../darwin/continuous-build-darwin.yml | 2 +- .../darwin/helper-plugin-entitlements.plist | 10 - .../darwin/product-build-darwin.yml | 200 +- build/azure-pipelines/darwin/publish.sh | 1 + build/azure-pipelines/distro-build.yml | 2 +- build/azure-pipelines/exploration-build.yml | 2 +- build/azure-pipelines/linux/Dockerfile | 6 +- build/azure-pipelines/linux/alpine/publish.sh | 2 +- .../linux/continuous-build-linux.yml | 11 +- .../linux/product-build-alpine.yml | 103 +- .../linux/product-build-linux.yml | 169 +- build/azure-pipelines/linux/publish.sh | 15 + .../linux/snap-build-linux.yml | 10 +- .../linux/sql-product-build-linux.yml | 31 +- build/azure-pipelines/product-build.yml | 438 ++- build/azure-pipelines/product-compile.yml | 106 +- .../azure-pipelines/publish-types/.gitignore | 2 - .../publish-types/check-version.js | 36 + .../publish-types/publish-types.yml | 2 +- .../publish-types/update-types.js | 72 + build/azure-pipelines/sql-product-build.yml | 2 +- build/azure-pipelines/sync-mooncake.yml | 2 +- .../azure-pipelines/web/product-build-web.yml | 96 +- .../win32/ESRPClient/NuGet.config | 11 +- .../win32/continuous-build-win32.yml | 2 +- .../win32/import-esrp-auth-cert.ps1 | 2 +- .../win32/product-build-win32.yml | 152 +- build/builtin/browser-main.js | 17 +- build/builtin/main.js | 15 +- build/darwin/create-universal-app.js | 58 + build/darwin/create-universal-app.ts | 66 + build/darwin/sign.js | 4 - build/darwin/sign.ts | 10 - build/eslint.js | 36 + build/filters.js | 152 + build/gulpfile.compile.js | 7 +- build/gulpfile.editor.js | 5 +- build/gulpfile.extensions.js | 11 +- build/gulpfile.js | 41 + build/gulpfile.reh.js | 8 +- build/gulpfile.vscode.js | 37 +- build/gulpfile.vscode.linux.js | 6 +- build/lib/builtInExtensions.js | 201 +- build/lib/builtInExtensions.ts | 163 + build/lib/compilation.js | 8 +- build/lib/compilation.ts | 10 +- build/lib/dependencies.js | 60 + .../{dependencies.js => lib/dependencies.ts} | 40 +- build/lib/electron.js | 4 +- build/lib/electron.ts | 6 +- build/lib/eslint/vscode-dts-cancellation.js | 33 + build/lib/eslint/vscode-dts-cancellation.ts | 38 + .../lib/eslint/vscode-dts-provider-naming.js | 38 + .../lib/eslint/vscode-dts-provider-naming.ts | 45 + .../lib/eslint/vscode-dts-region-comments.js | 35 + .../lib/eslint/vscode-dts-region-comments.ts | 41 + build/lib/eslint/vscode-dts-use-thenable.js | 24 + build/lib/eslint/vscode-dts-use-thenable.ts | 30 + build/lib/extensions.js | 24 +- build/lib/extensions.ts | 26 +- build/lib/i18n.resources.json | 12 + build/lib/layersChecker.js | 2 - build/lib/layersChecker.ts | 2 - build/lib/monaco-api.js | 67 +- build/lib/monaco-api.ts | 753 +++++ build/lib/nls.js | 50 +- build/lib/nls.ts | 60 +- build/lib/node.js | 6 +- build/lib/node.ts | 10 +- build/lib/optimize.js | 79 +- build/lib/optimize.ts | 97 +- build/lib/standalone.js | 3 +- build/lib/standalone.ts | 5 +- build/lib/stats.js | 2 +- build/lib/stats.ts | 2 +- build/lib/treeshaking.js | 57 +- build/lib/treeshaking.ts | 59 +- build/lib/watch/index.js | 5 +- .../lib/watch/index.ts | 13 +- build/lib/watch/package.json | 2 +- build/lib/watch/watch-win32.js | 187 +- build/lib/watch/watch-win32.ts | 108 + build/lib/watch/yarn.lock | 453 +-- build/monaco/monaco.d.ts.recipe | 1 + build/monaco/package.json | 2 +- build/npm/dirs.js | 51 + build/npm/preinstall.js | 34 + build/npm/update-all-grammars.js | 67 +- build/npm/update-theme.js | 82 - build/package.json | 45 +- build/yarn.lock | 2997 ++++------------ cgmanifest.json | 16 +- extensions/bat/package.json | 72 +- extensions/bat/package.nls.json | 2 +- extensions/bat/yarn.lock | 4 + extensions/big-data-cluster/.vscodeignore | 10 +- .../extension.webpack.config.js | 2 +- extensions/big-data-cluster/package.json | 2 +- .../src/bigDataCluster/auth.ts | 2 +- extensions/big-data-cluster/yarn.lock | 6 +- extensions/configuration-editing/package.json | 6 +- .../configuration-editing/package.nls.json | 2 +- .../src/configurationEditingMain.ts | 4 +- .../src/extensionsProposals.ts | 4 +- .../src/settingsDocumentHelper.ts | 11 +- extensions/configuration-editing/yarn.lock | 8 +- extensions/dacpac/src/test/testContext.ts | 3 +- extensions/docker/package.json | 87 +- extensions/docker/package.nls.json | 2 +- extensions/docker/yarn.lock | 4 + extensions/extension-editing/package.json | 131 +- extensions/extension-editing/package.nls.json | 2 +- extensions/extension-editing/yarn.lock | 8 +- extensions/git-ui/package.json | 7 +- extensions/git-ui/package.nls.json | 2 +- extensions/git-ui/yarn.lock | 8 +- extensions/git/build/update-grammars.js | 2 +- extensions/git/package.json | 34 +- extensions/git/package.nls.json | 5 +- extensions/git/src/api/api1.ts | 15 +- extensions/git/src/api/git.d.ts | 15 +- extensions/git/src/askpass-main.ts | 2 +- extensions/git/src/autofetch.ts | 34 +- extensions/git/src/commands.ts | 94 +- extensions/git/src/git.ts | 250 +- extensions/git/src/main.ts | 15 +- extensions/git/src/model.ts | 10 +- extensions/git/src/repository.ts | 93 +- extensions/git/src/test/smoke.test.ts | 27 + extensions/github-authentication/package.json | 13 +- .../src/common/keychain.ts | 9 +- .../github-authentication/src/extension.ts | 9 +- .../github-authentication/src/github.ts | 16 +- extensions/github-authentication/yarn.lock | 463 +-- extensions/github/package.json | 7 +- extensions/github/yarn.lock | 8 +- extensions/image-preview/package.json | 6 +- extensions/import/src/test/utils.test.ts | 1 + .../client/src/jsonClient.ts | 19 +- .../json-language-features/package.json | 258 +- .../server/package.json | 10 +- .../server/src/jsonServer.ts | 10 +- .../server/src/utils/runner.ts | 4 +- .../json-language-features/server/yarn.lock | 74 +- extensions/json-language-features/yarn.lock | 96 +- extensions/json/build/update-grammars.js | 4 +- extensions/json/package.json | 4 + extensions/json/package.nls.json | 2 +- extensions/json/yarn.lock | 4 + .../src/test/mainController.test.ts | 3 +- extensions/markdown-basics/cgmanifest.json | 6 +- extensions/markdown-basics/package.json | 190 +- .../syntaxes/markdown.tmLanguage.json | 4 +- extensions/markdown-basics/yarn.lock | 4 + .../markdown-language-features/media/index.js | 2 +- .../markdown-language-features/media/pre.js | 2 +- .../markdown-language-features/package.json | 8 +- .../preview-src/index.ts | 42 +- .../preview-src/scroll-sync.ts | 1 + .../src/commands/renderDocument.ts | 2 +- .../src/features/preview.ts | 51 +- .../src/features/previewContentProvider.ts | 17 +- .../src/features/previewManager.ts | 7 +- .../src/features/smartSelect.ts | 106 +- .../src/markdownEngine.ts | 44 +- .../src/test/documentLink.test.ts | 5 + .../src/test/engine.test.ts | 23 +- .../src/test/smartSelect.test.ts | 64 +- .../src/test/urlToUri.test.ts | 39 + .../src/util/url.ts | 25 + .../markdown-language-features/yarn.lock | 71 +- extensions/merge-conflict/package.json | 8 +- extensions/merge-conflict/yarn.lock | 8 +- .../microsoft-authentication/package.json | 117 +- .../microsoft-authentication/src/AADHelper.ts | 51 +- .../microsoft-authentication/src/extension.ts | 9 +- .../microsoft-authentication/src/keychain.ts | 11 +- .../src/microsoft-authentication.d.ts | 16 + extensions/microsoft-authentication/yarn.lock | 437 +-- extensions/mssql/.vscodeignore | 10 +- extensions/mssql/extension.webpack.config.js | 2 +- extensions/mssql/package.json | 2 +- extensions/mssql/src/util/auth.ts | 2 +- extensions/mssql/yarn.lock | 6 +- extensions/notebook/src/test/common/stubs.ts | 1 + extensions/package.json | 6 +- extensions/powershell/package.json | 79 +- extensions/powershell/package.nls.json | 2 +- extensions/powershell/yarn.lock | 4 + extensions/python/.vscodeignore | 5 - extensions/python/language-configuration.json | 8 +- extensions/python/package.json | 94 +- extensions/python/package.nls.json | 2 +- extensions/python/yarn.lock | 4 + extensions/r/package.json | 64 +- extensions/r/package.nls.json | 2 +- extensions/r/yarn.lock | 4 + .../resource-deployment/src/test/stubs.ts | 6 +- .../schema-compare/src/test/testContext.ts | 3 +- extensions/search-result/package.json | 6 +- extensions/search-result/src/extension.ts | 25 +- extensions/shared.webpack.config.js | 18 +- extensions/simple-browser/.vscodeignore | 12 + extensions/simple-browser/README.md | 3 + .../extension-browser.webpack.config.js | 5 +- .../extension.webpack.config.js | 5 +- extensions/simple-browser/media/index.js | 210 ++ extensions/simple-browser/media/index.js.map | 1 + extensions/simple-browser/media/main.css | 115 + .../simple-browser/media/preview-dark.svg | 3 + .../simple-browser/media/preview-light.svg | 3 + extensions/simple-browser/package.json | 73 + extensions/simple-browser/package.nls.json | 5 + .../simple-browser/preview-src/events.ts | 12 + .../simple-browser/preview-src/index.ts | 97 + .../simple-browser/preview-src/tsconfig.json | 12 + extensions/simple-browser/src/dispose.ts | 42 + extensions/simple-browser/src/extension.ts | 74 + .../src/simpleBrowserManager.ts | 38 + .../simple-browser/src/simpleBrowserView.ts | 154 + .../simple-browser/src/typings/ref.d.ts | 6 +- extensions/simple-browser/tsconfig.json | 10 + .../webpack.config.js} | 29 +- extensions/simple-browser/yarn.lock | 2732 +++++++++++++++ .../src/test/testContext.ts | 3 +- extensions/sql/build/update-grammar.js | 2 +- extensions/sql/package.json | 72 +- extensions/sql/package.nls.json | 2 +- extensions/sql/yarn.lock | 4 + .../.vscodeignore | 6 + .../testing-editor-contributions/README.md | 5 + .../extension-browser.webpack.config.js | 22 + .../extension.webpack.config.js | 20 + .../testing-editor-contributions/package.json | 52 + .../package.nls.json | 19 + .../src/extension.ts | 412 +++ .../src/typings/refs.d.ts | 5 +- .../tsconfig.json | 4 +- .../testing-editor-contributions/yarn.lock | 8 + extensions/theme-abyss/package.json | 40 +- extensions/theme-abyss/yarn.lock | 4 + extensions/theme-defaults/package.json | 6 +- extensions/theme-defaults/themes/dark_vs.json | 21 +- .../theme-defaults/themes/hc_black.json | 325 +- .../themes/hc_black_defaults.json | 340 -- .../theme-defaults/themes/light_defaults.json | 28 - .../theme-defaults/themes/light_plus.json | 3 +- .../theme-defaults/themes/light_vs.json | 25 +- extensions/theme-defaults/yarn.lock | 4 + extensions/theme-kimbie-dark/package.json | 40 +- extensions/theme-kimbie-dark/yarn.lock | 4 + extensions/theme-monokai-dimmed/package.json | 42 +- extensions/theme-monokai-dimmed/yarn.lock | 4 + extensions/theme-monokai/package.json | 42 +- extensions/theme-monokai/yarn.lock | 4 + extensions/theme-quietlight/package.json | 42 +- extensions/theme-quietlight/yarn.lock | 4 + extensions/theme-red/package.json | 40 +- extensions/theme-red/yarn.lock | 4 + extensions/theme-seti/.vscodeignore | 1 + .../theme-seti/build/update-icon-theme.js | 1 + extensions/theme-seti/cgmanifest.json | 2 +- extensions/theme-seti/icons/preview.html | 104 + extensions/theme-seti/icons/seti.woff | Bin 35676 -> 35844 bytes .../theme-seti/icons/vs-seti-icon-theme.json | 638 ++-- extensions/theme-seti/package.json | 48 +- extensions/theme-seti/yarn.lock | 4 + extensions/theme-solarized-dark/package.json | 40 +- extensions/theme-solarized-dark/yarn.lock | 4 + extensions/theme-solarized-light/package.json | 40 +- extensions/theme-solarized-light/yarn.lock | 4 + .../theme-tomorrow-night-blue/package.json | 40 +- .../theme-tomorrow-night-blue/yarn.lock | 4 + .../build/update-grammars.js | 84 + extensions/typescript-basics/yarn.lock | 4 + .../test/colorize-fixtures/Dockerfile | 0 .../test/colorize-fixtures/issue-1550.yaml | 0 .../test/colorize-fixtures/issue-4008.yaml | 0 .../test/colorize-fixtures/issue-6303.yaml | 0 .../test/colorize-fixtures/test-33886.md | 0 .../test/colorize-fixtures/test-7115.xml | 0 .../colorize-fixtures/test-freeze-56476.ps1 | 0 .../test/colorize-fixtures/test.bat | 0 .../test/colorize-fixtures/test.json | 0 .../test/colorize-fixtures/test.md | 0 .../test/colorize-fixtures/test.ps1 | 0 .../test/colorize-fixtures/test.r | 0 .../test/colorize-fixtures/test.sql | 0 .../test/colorize-fixtures/test.xml | 0 .../test/colorize-fixtures/test.yaml | 0 .../test/colorize-results/Dockerfile.json | 0 .../colorize-results/issue-1550_yaml.json | 0 .../colorize-results/issue-4008_yaml.json | 0 .../colorize-results/issue-6303_yaml.json | 0 .../test/colorize-results/test-33886_md.json | 0 .../test/colorize-results/test-7115_xml.json | 0 .../test-freeze-56476_ps1.json | 0 .../test/colorize-results/test_bat.json | 0 .../test/colorize-results/test_json.json | 0 .../test/colorize-results/test_md.json | 0 .../test/colorize-results/test_ps1.json | 0 .../test/colorize-results/test_r.json | 0 .../test/colorize-results/test_sql.json | 0 .../test/colorize-results/test_xml.json | 0 .../test/colorize-results/test_yaml.json | 0 .../src/notebook.test.ts | 111 +- .../vscode-test-resolver/src/download.ts | 21 +- .../vscode-test-resolver/src/extension.ts | 164 +- .../src/util/processes.ts | 4 +- extensions/xml/package.json | 212 +- extensions/xml/package.nls.json | 2 +- extensions/xml/yarn.lock | 4 + extensions/yaml/package.json | 94 +- extensions/yaml/package.nls.json | 2 +- extensions/yaml/yarn.lock | 4 + extensions/yarn.lock | 33 +- gulpfile.js | 39 +- package.json | 98 +- product.json | 3 +- remote/.yarnrc | 2 +- remote/package.json | 15 +- remote/web/package.json | 7 +- remote/web/yarn.lock | 24 +- remote/yarn.lock | 68 +- resources/completions/zsh/_code | 4 +- resources/linux/debian/postinst.template | 7 +- resources/web/code-web.js | 59 +- scripts/test-extensions-unit.bat | 85 +- scripts/test-extensions-unit.js | 15 +- scripts/test-extensions-unit.sh | 100 +- scripts/test-integration.bat | 57 +- scripts/test-integration.sh | 70 +- scripts/test.sh | 4 +- src/bootstrap-amd.js | 2 + src/bootstrap-fork.js | 56 +- src/bootstrap-node.js | 2 +- src/bootstrap-window.js | 117 +- src/bootstrap.js | 101 +- src/buildfile.js | 2 +- src/main.js | 67 +- .../ui/table/plugins/headerFilter.plugin.ts | 2 +- .../ui/taskbar/overflowActionbar.test.ts | 115 +- .../editableDropdown/browser/dropdown.test.ts | 24 +- .../browser/actions/layoutActions.ts | 35 + src/sql/workbench/browser/modal/modal.ts | 8 +- .../modelComponents/button.component.ts | 2 +- .../modelComponents/queryTextEditor.ts | 4 +- .../modelComponents/treeViewDataProvider.ts | 2 +- .../test/browser/asmtActions.test.ts | 2 +- .../browser/connectionViewletPanel.ts | 2 +- .../browser/dataExplorer.contribution.ts | 17 +- .../browser/dataExplorerViewlet.ts | 34 +- .../browser/find/notebookFindModel.ts | 6 +- .../notebook/browser/notebook.component.ts | 8 +- .../notebook/browser/notebook.contribution.ts | 12 +- .../notebookExplorerViewlet.ts | 25 +- .../notebookExplorer/notebookSearch.ts | 9 +- .../electron-browser/contentManagers.test.ts | 8 +- .../notebookEditorModel.test.ts | 2 +- .../opener/common/openerServiceStub.ts | 10 + .../browser/profilerResourceEditor.ts | 4 +- .../query/browser/queryResultsEditor.ts | 5 +- .../browser/queryHistoryActions.ts | 2 +- .../queryHistory/browser/queryHistoryView.ts | 2 +- .../electron-browser/queryHistory.ts | 5 +- .../browser/resourceViewer.contribution.ts | 2 +- .../browser/resourceViewerView.ts | 2 +- .../tasks/browser/tasks.contribution.ts | 7 +- .../contrib/tasks/browser/tasksActions.ts | 2 +- .../contrib/tasks/browser/tasksView.ts | 2 +- .../browser/accountDialog.ts | 5 +- .../browser/connectionDialogService.test.ts | 2 +- .../browser/connectionDialogWidget.test.ts | 2 +- .../connection/test/browser/testTreeView.ts | 2 +- .../browser/errorMessageDialog.ts | 2 +- .../insights/browser/insightsDialogView.ts | 5 +- .../browser/insightsDialogController.test.ts | 26 +- .../electron-browser/insightsUtils.test.ts | 5 +- .../services/tasks/common/tasksService.ts | 4 +- src/tsconfig.tsec.json | 16 + src/tsec.exemptions.json | 5 + src/typings/require.d.ts | 1 + src/vs/base/browser/browser.ts | 6 +- src/vs/base/browser/canIUse.ts | 2 +- src/vs/base/browser/contextmenu.ts | 2 +- src/vs/base/browser/dom.ts | 85 +- src/vs/base/browser/event.ts | 14 +- src/vs/base/browser/hash.ts | 9 +- src/vs/base/browser/markdownRenderer.ts | 26 +- .../base/browser/ui/actionbar/actionbar.css | 4 - .../ui/breadcrumbs/breadcrumbsWidget.ts | 28 +- src/vs/base/browser/ui/button/button.ts | 28 +- src/vs/base/browser/ui/checkbox/checkbox.ts | 8 +- .../ui/codicons/codicon/codicon-modifiers.css | 30 + .../browser/ui/codicons/codicon/codicon.ttf | Bin 62836 -> 64268 bytes .../base/browser/ui/codicons/codiconStyles.ts | 3 +- src/vs/base/browser/ui/dialog/dialog.ts | 10 +- src/vs/base/browser/ui/dropdown/dropdown.ts | 8 +- .../ui/dropdown/dropdownActionViewItem.ts | 8 +- src/vs/base/browser/ui/findinput/findInput.ts | 4 + .../ui/highlightedlabel/highlightedLabel.ts | 10 +- src/vs/base/browser/ui/hover/hover.css | 7 +- src/vs/base/browser/ui/hover/hoverWidget.ts | 4 +- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 86 +- .../iconLabel/iconLabels.ts} | 18 +- .../base/browser/ui/iconLabel/iconlabel.css | 18 +- .../simpleIconLabel.ts} | 6 +- src/vs/base/browser/ui/inputbox/inputBox.css | 19 +- src/vs/base/browser/ui/inputbox/inputBox.ts | 9 +- src/vs/base/browser/ui/list/list.css | 7 +- src/vs/base/browser/ui/list/listPaging.ts | 4 + src/vs/base/browser/ui/list/listView.ts | 148 +- src/vs/base/browser/ui/list/listWidget.ts | 34 +- src/vs/base/browser/ui/list/rowCache.ts | 3 +- src/vs/base/browser/ui/menu/menu.ts | 5 +- src/vs/base/browser/ui/menu/menubar.ts | 6 +- .../browser/ui/scrollbar/abstractScrollbar.ts | 16 +- .../ui/scrollbar/horizontalScrollbar.ts | 4 +- .../browser/ui/scrollbar/scrollableElement.ts | 31 +- .../ui/scrollbar/scrollableElementOptions.ts | 6 + .../browser/ui/scrollbar/scrollbarArrow.ts | 2 +- .../browser/ui/scrollbar/scrollbarState.ts | 12 +- .../scrollbarVisibilityController.ts | 4 +- .../browser/ui/scrollbar/verticalScrollbar.ts | 4 +- src/vs/base/browser/ui/toolbar/toolbar.ts | 2 +- src/vs/base/browser/ui/tree/abstractTree.ts | 4 +- src/vs/base/browser/ui/tree/asyncDataTree.ts | 44 +- .../ui/tree/compressedObjectTreeModel.ts | 62 +- src/vs/base/browser/ui/tree/dataTree.ts | 10 +- src/vs/base/browser/ui/tree/indexTreeModel.ts | 116 +- src/vs/base/browser/ui/tree/objectTree.ts | 29 +- .../base/browser/ui/tree/objectTreeModel.ts | 32 +- src/vs/base/common/actions.ts | 62 +- src/vs/base/common/async.ts | 232 +- src/vs/base/common/buffer.ts | 5 +- src/vs/base/common/buildunit.json | 16 - src/vs/base/common/codicons.ts | 59 +- src/vs/base/common/errors.ts | 9 + src/vs/base/common/event.ts | 129 +- src/vs/base/common/extpath.ts | 17 +- src/vs/base/common/filters.ts | 389 ++- src/vs/base/common/fuzzyScorer.ts | 2 +- src/vs/base/common/glob.ts | 23 +- src/vs/base/common/htmlContent.ts | 11 +- src/vs/base/common/iconLabels.ts | 159 + src/vs/base/common/iterator.ts | 41 + src/vs/base/common/keyCodes.ts | 11 + src/vs/base/common/labels.ts | 21 +- src/vs/base/common/lifecycle.ts | 77 +- src/vs/base/common/map.ts | 3 +- src/vs/base/common/network.ts | 44 +- src/vs/base/common/paging.ts | 15 - src/vs/base/common/path.ts | 4 +- src/vs/base/common/performance.d.ts | 12 +- src/vs/base/common/performance.js | 111 +- src/vs/base/common/platform.ts | 25 +- src/vs/base/common/processes.ts | 1 - src/vs/base/common/resources.ts | 4 +- src/vs/base/common/scrollable.ts | 33 +- src/vs/base/common/stopwatch.ts | 2 +- src/vs/base/common/stream.ts | 66 +- src/vs/base/common/strings.ts | 18 +- src/vs/base/common/uri.ts | 2 +- src/vs/base/node/extpath.ts | 5 +- src/vs/base/node/languagePacks.js | 84 +- src/vs/base/node/pfs.ts | 725 ++-- src/vs/base/node/powershell.ts | 316 ++ src/vs/base/node/processes.ts | 18 +- src/vs/base/node/shell.ts | 93 + src/vs/base/node/zip.ts | 8 +- src/vs/base/parts/ipc/browser/ipc.mp.ts | 22 + src/vs/base/parts/ipc/common/ipc.electron.ts | 7 +- src/vs/base/parts/ipc/common/ipc.mp.ts | 78 + src/vs/base/parts/ipc/common/ipc.net.ts | 20 +- src/vs/base/parts/ipc/common/ipc.ts | 8 + .../{ipc.electron-main.ts => ipc.electron.ts} | 9 +- src/vs/base/parts/ipc/electron-main/ipc.mp.ts | 57 + ...pc.electron-sandbox.ts => ipc.electron.ts} | 16 +- .../base/parts/ipc/electron-sandbox/ipc.mp.ts | 51 + src/vs/base/parts/ipc/node/ipc.net.ts | 188 +- .../parts/ipc/test/browser/ipc.mp.test.ts | 58 + .../ipc/test/electron-sandbox/ipc.mp.test.ts | 30 + .../base/parts/ipc/test/node/ipc.cp.test.ts | 2 +- .../base/parts/ipc/test/node/ipc.net.test.ts | 56 +- .../quickinput/browser/media/quickInput.css | 2 +- .../parts/quickinput/browser/quickInput.ts | 8 +- .../quickinput/browser/quickInputList.ts | 25 +- .../parts/quickinput/common/quickInput.ts | 1 + .../parts/sandbox/common/electronTypes.ts | 8 +- .../parts/sandbox/electron-browser/preload.js | 12 + .../sandbox/electron-sandbox/electronTypes.ts | 96 +- .../parts/sandbox/electron-sandbox/globals.ts | 55 +- .../test/electron-sandbox/globals.test.ts | 9 +- src/vs/base/parts/storage/common/storage.ts | 22 +- src/vs/base/parts/storage/node/storage.ts | 23 +- .../parts/storage/test/node/storage.test.ts | 375 +- src/vs/base/test/browser/actionbar.test.ts | 22 +- src/vs/base/test/browser/codicons.test.ts | 52 - src/vs/base/test/browser/comparers.test.ts | 10 +- src/vs/base/test/browser/dom.test.ts | 37 +- src/vs/base/test/browser/hash.test.ts | 68 +- .../test/browser/highlightedLabel.test.ts | 23 +- src/vs/base/test/browser/iconLabels.test.ts | 52 + .../ui/contextview/contextview.test.ts | 24 +- .../ui/scrollbar/scrollableElement.test.ts | 16 +- .../ui/scrollbar/scrollbarState.test.ts | 64 +- .../ui/tree/compressedObjectTreeModel.test.ts | 47 +- .../browser/ui/tree/indexTreeModel.test.ts | 207 +- .../test/browser/ui/tree/objectTree.test.ts | 168 +- src/vs/base/test/common/arrays.test.ts | 174 +- src/vs/base/test/common/async.test.ts | 396 ++- .../base/test/{node => common}/buffer.test.ts | 142 +- src/vs/base/test/common/cancellation.test.ts | 24 +- src/vs/base/test/common/charCode.test.ts | 2 +- .../test/{node => common}/console.test.ts | 2 +- src/vs/base/test/common/diff/diff.test.ts | 14 +- src/vs/base/test/common/event.test.ts | 237 +- src/vs/base/test/common/extpath.test.ts | 192 +- src/vs/base/test/common/filters.perf.test.ts | 7 +- src/vs/base/test/common/filters.test.ts | 27 +- src/vs/base/test/common/fuzzyScorer.test.ts | 688 ++-- .../base/test/{node => common}/glob.test.ts | 15 +- src/vs/base/test/common/iconLabels.test.ts | 88 + src/vs/base/test/common/keyCodes.test.ts | 2 +- src/vs/base/test/common/labels.test.ts | 173 +- src/vs/base/test/common/lifecycle.test.ts | 25 +- src/vs/base/test/common/linkedList.test.ts | 15 +- src/vs/base/test/common/linkedText.test.ts | 30 +- src/vs/base/test/common/map.test.ts | 490 +-- .../base/test/common/markdownString.test.ts | 18 +- src/vs/base/test/common/mime.test.ts | 50 +- src/vs/base/test/common/network.test.ts | 28 +- src/vs/base/test/common/paging.test.ts | 1 - .../base/test/{node => common}/path.test.ts | 8 +- src/vs/base/test/common/processes.test.ts | 1 - src/vs/base/test/common/resources.test.ts | 256 +- src/vs/base/test/common/scrollable.test.ts | 2 +- src/vs/base/test/common/skipList.test.ts | 45 +- src/vs/base/test/common/stream.test.ts | 113 +- src/vs/base/test/common/strings.test.ts | 129 +- src/vs/base/test/common/troubleshooting.ts | 48 + src/vs/base/test/common/types.test.ts | 18 +- src/vs/base/test/common/uri.test.ts | 496 +-- src/vs/base/test/common/utils.ts | 39 +- src/vs/base/test/common/uuid.test.ts | 4 +- src/vs/base/test/node/crypto.test.ts | 24 +- src/vs/base/test/node/decoder.test.ts | 14 +- src/vs/base/test/node/extpath.test.ts | 69 +- src/vs/base/test/node/id.test.ts | 21 +- src/vs/base/test/node/keytar.test.ts | 42 +- src/vs/base/test/node/pfs/pfs.test.ts | 541 ++- src/vs/base/test/node/port.test.ts | 10 +- src/vs/base/test/node/powershell.test.ts | 83 + .../test/node/processes/processes.test.ts | 12 +- src/vs/base/test/node/testUtils.ts | 16 + src/vs/base/test/node/zip/zip.test.ts | 32 +- src/vs/base/worker/defaultWorkerFactory.ts | 7 +- src/vs/buildunit.json | 13 - .../code/browser/workbench/workbench-dev.html | 5 +- src/vs/code/browser/workbench/workbench.html | 5 +- src/vs/code/browser/workbench/workbench.ts | 1 - .../contrib/deprecatedExtensionsCleaner.ts | 25 + .../contrib/languagePackCachedDataCleaner.ts | 5 +- .../contrib/localizationsUpdater.ts | 23 + .../sharedProcess/contrib/logsDataCleaner.ts | 3 +- .../contrib/nodeCachedDataCleaner.ts | 5 +- .../contrib/storageDataCleaner.ts | 5 +- .../sharedProcess/sharedProcess.js | 5 +- .../sharedProcess/sharedProcessMain.ts | 432 +-- .../electron-browser/workbench/workbench.js | 41 +- src/vs/code/electron-main/app.ts | 156 +- src/vs/code/electron-main/auth.ts | 250 +- src/vs/code/electron-main/auth2.ts | 242 -- src/vs/code/electron-main/main.ts | 39 +- src/vs/code/electron-main/protocol.ts | 10 +- src/vs/code/electron-main/sharedProcess.ts | 267 +- src/vs/code/electron-main/window.ts | 223 +- .../issue/issueReporterMain.ts | 75 +- .../processExplorer/media/processExplorer.css | 39 +- .../processExplorer/processExplorer.html | 2 +- .../processExplorer/processExplorerMain.ts | 482 +-- src/vs/code/electron-sandbox/proxy/auth.html | 83 - src/vs/code/electron-sandbox/proxy/auth.js | 48 - .../electron-sandbox/workbench/workbench.html | 3 +- .../electron-sandbox/workbench/workbench.js | 30 +- src/vs/code/node/cli.ts | 30 +- src/vs/code/node/cliProcessMain.ts | 508 +-- src/vs/code/node/shellEnv.ts | 8 +- src/vs/editor/browser/config/configuration.ts | 52 +- .../editor/browser/controller/coreCommands.ts | 2 + .../editor/browser/controller/mouseHandler.ts | 7 +- .../editor/browser/controller/mouseTarget.ts | 32 +- .../browser/controller/textAreaHandler.ts | 14 +- .../browser/controller/textAreaInput.ts | 92 +- .../browser/controller/textAreaState.ts | 24 +- .../editor/browser/core/markdownRenderer.ts | 4 +- src/vs/editor/browser/editorBrowser.ts | 22 +- .../browser/services/bulkEditService.ts | 1 + .../browser/services/codeEditorServiceImpl.ts | 5 +- .../editor/browser/services/openerService.ts | 46 +- .../browser/view/domLineBreaksComputer.ts | 14 +- src/vs/editor/browser/view/viewImpl.ts | 26 +- src/vs/editor/browser/view/viewLayer.ts | 14 +- .../currentLineHighlight.ts | 4 +- .../viewParts/lineNumbers/lineNumbers.ts | 35 +- .../browser/viewParts/lines/viewLine.ts | 2 +- .../browser/viewParts/minimap/minimap.css | 5 + .../browser/viewParts/minimap/minimap.ts | 8 +- .../viewParts/selections/selections.ts | 2 +- .../viewParts/viewCursors/viewCursors.ts | 17 +- .../editor/browser/widget/codeEditorWidget.ts | 35 +- .../editor/browser/widget/diffEditorWidget.ts | 150 +- src/vs/editor/browser/widget/diffReview.ts | 18 +- .../widget/embeddedCodeEditorWidget.ts | 2 +- .../common/config/commonEditorConfig.ts | 11 +- src/vs/editor/common/config/editorOptions.ts | 141 +- src/vs/editor/common/config/fontInfo.ts | 21 +- src/vs/editor/common/controller/cursor.ts | 6 +- .../common/controller/cursorMoveCommands.ts | 43 +- .../common/controller/cursorMoveOperations.ts | 52 +- .../common/controller/cursorTypeOperations.ts | 11 +- src/vs/editor/common/diff/diffComputer.ts | 7 + src/vs/editor/common/editorCommon.ts | 8 +- src/vs/editor/common/editorContextKeys.ts | 1 + src/vs/editor/common/model.ts | 30 +- src/vs/editor/common/model/editStack.ts | 17 + .../pieceTreeTextBufferBuilder.ts | 6 +- src/vs/editor/common/model/textModel.ts | 37 +- src/vs/editor/common/model/textModelTokens.ts | 10 +- src/vs/editor/common/model/tokensStore.ts | 4 + src/vs/editor/common/modes.ts | 33 +- .../common/modes/languageConfiguration.ts | 2 +- .../modes/languageConfigurationRegistry.ts | 168 +- src/vs/editor/common/modes/modesRegistry.ts | 2 +- .../editor/common/modes/supports/onEnter.ts | 6 +- .../common/modes/textToHtmlTokenizer.ts | 6 +- .../common/services/getSemanticTokens.ts | 160 + .../services/markerDecorationsServiceImpl.ts | 8 +- .../services/markersDecorationService.ts | 5 +- src/vs/editor/common/services/modeService.ts | 2 +- .../editor/common/services/modeServiceImpl.ts | 11 +- .../common/services/modelServiceImpl.ts | 162 +- .../common/services}/semanticTokensDto.ts | 0 .../common/standalone/standaloneEnums.ts | 28 +- src/vs/editor/common/view/viewEvents.ts | 16 +- src/vs/editor/common/viewLayout/viewLayout.ts | 2 +- .../viewModel/monospaceLineBreaksComputer.ts | 17 + .../common/viewModel/splitLinesCollection.ts | 11 +- .../common/viewModel/viewEventHandler.ts | 19 +- src/vs/editor/common/viewModel/viewModel.ts | 2 + .../editor/common/viewModel/viewModelImpl.ts | 12 +- .../test/bracketMatching.test.ts | 56 +- src/vs/editor/contrib/clipboard/clipboard.ts | 5 +- .../contrib/codelens/codelensController.ts | 2 +- .../editor/contrib/codelens/codelensWidget.ts | 4 +- .../contrib/colorPicker/colorContributions.ts | 2 +- .../comment/test/lineCommentCommand.test.ts | 64 +- .../cursorUndo/test/cursorUndo.test.ts | 14 +- src/vs/editor/contrib/dnd/dnd.ts | 5 +- .../documentSymbols.ts | 50 +- .../editor/contrib/documentSymbols/outline.ts | 18 - .../contrib/documentSymbols/outlineModel.ts | 43 +- src/vs/editor/contrib/find/findWidget.css | 10 +- src/vs/editor/contrib/find/findWidget.ts | 4 +- src/vs/editor/contrib/find/test/find.test.ts | 18 +- .../contrib/find/test/findController.test.ts | 68 +- .../contrib/find/test/findModel.test.ts | 302 +- .../contrib/find/test/replacePattern.test.ts | 18 +- .../contrib/gotoError/gotoErrorWidget.ts | 19 +- .../contrib/gotoSymbol/referencesModel.ts | 2 +- src/vs/editor/contrib/hover/hover.ts | 84 +- src/vs/editor/contrib/hover/hoverWidgets.ts | 161 +- .../contrib/hover/markdownHoverParticipant.ts | 126 + .../contrib/hover/markerHoverParticipant.ts | 257 ++ .../editor/contrib/hover/modesContentHover.ts | 595 ++-- .../editor/contrib/indentation/indentation.ts | 10 +- .../inlineHints/inlineHintsController.ts | 227 ++ .../linesOperations/linesOperations.ts | 48 +- .../linesOperations/moveLinesCommand.ts | 6 +- .../test/copyLinesCommand.test.ts | 4 +- .../test/linesOperations.test.ts | 387 ++- .../contrib/linkedEditing/linkedEditing.ts | 1 + src/vs/editor/contrib/links/links.ts | 2 +- .../editor/contrib/multicursor/multicursor.ts | 6 - .../multicursor/test/multicursor.test.ts | 86 +- .../contrib/parameterHints/parameterHints.css | 2 +- .../parameterHints/parameterHintsWidget.ts | 23 +- .../parameterHints/provideSignatureHelp.ts | 20 +- src/vs/editor/contrib/peekView/peekView.ts | 24 +- .../quickAccess/commandsQuickAccess.ts | 4 +- .../quickAccess/gotoSymbolQuickAccess.ts | 49 +- .../contrib/smartSelect/bracketSelections.ts | 2 +- .../smartSelect/test/smartSelect.test.ts | 12 +- .../contrib/snippet/snippetVariables.ts | 21 +- .../test/snippetController2.old.test.ts | 112 +- .../snippet/test/snippetController2.test.ts | 18 +- .../snippet/test/snippetParser.test.ts | 4 +- .../snippet/test/snippetSession.test.ts | 130 +- .../snippet/test/snippetVariables.test.ts | 74 +- src/vs/editor/contrib/suggest/suggestModel.ts | 8 +- .../editor/contrib/suggest/suggestWidget.ts | 38 +- .../contrib/suggest/suggestWidgetRenderer.ts | 4 +- .../contrib/suggest/suggestWidgetStatus.ts | 46 +- .../suggest/test/suggestController.test.ts | 1 + .../contrib/suggest/test/suggestModel.test.ts | 2 +- .../symbolIcons.ts} | 353 +- .../viewportSemanticTokens.ts | 16 +- .../test/wordOperations.test.ts | 162 +- .../contrib/wordOperations/wordOperations.ts | 2 +- .../test/wordPartOperations.test.ts | 22 +- src/vs/editor/editor.all.ts | 3 +- src/vs/editor/editor.api.ts | 10 +- src/vs/editor/standalone/browser/colorizer.ts | 6 +- .../browser/inspectTokens/inspectTokens.ts | 10 +- .../standaloneGotoSymbolQuickAccess.ts | 2 + .../standalone/browser/simpleServices.ts | 24 +- .../standalone/browser/standalone-tokens.css | 13 + .../browser/standaloneCodeEditor.ts | 62 +- .../standalone/browser/standaloneEditor.ts | 56 +- .../standalone/browser/standaloneLanguages.ts | 51 +- .../browser/standaloneThemeServiceImpl.ts | 36 +- .../common/monarch/monarchCommon.ts | 1 + .../common/monarch/monarchCompile.ts | 25 +- .../standalone/common/monarch/monarchLexer.ts | 46 +- .../standalone/common/monarch/monarchTypes.ts | 9 + .../common/standaloneThemeService.ts | 4 + src/vs/editor/standalone/common/themes.ts | 2 +- .../test/browser/standaloneLanguages.test.ts | 11 +- .../standalone/test/monarch/monarch.test.ts | 201 +- .../browser/commands/shiftCommand.test.ts | 4 +- .../test/browser/commands/sideEditing.test.ts | 6 +- .../trimTrailingWhitespaceCommand.test.ts | 4 +- .../test/browser/controller/cursor.test.ts | 809 +++-- .../controller/cursorMoveCommand.test.ts | 97 +- .../browser/controller/textAreaState.test.ts | 26 +- .../test/browser/core/editorState.test.ts | 12 +- .../services/decorationRenderOptions.test.ts | 14 +- src/vs/editor/test/browser/testCodeEditor.ts | 9 +- .../browser/view/minimapCharRenderer.test.ts | 4 +- .../test/browser/view/viewLayer.test.ts | 10 +- .../common/config/commonEditorConfig.test.ts | 40 +- .../controller/cursorMoveHelper.test.ts | 60 +- .../common/core/characterClassifier.test.ts | 34 +- .../test/common/core/lineTokens.test.ts | 106 +- src/vs/editor/test/common/core/range.test.ts | 100 +- .../test/common/diff/diffComputer.test.ts | 9 +- .../test/common/mocks/testConfiguration.ts | 1 + .../common/model/benchmark/benchmarkUtils.ts | 2 +- .../common/model/editableTextModel.test.ts | 22 +- .../test/common/model/intervalTree.test.ts | 8 +- .../linesTextBuffer/linesTextBuffer.test.ts | 6 +- .../linesTextBufferBuilder.test.ts | 8 +- .../textBufferAutoTestUtils.ts | 21 +- .../test/common/model/model.line.test.ts | 6 +- .../test/common/model/model.modes.test.ts | 10 +- src/vs/editor/test/common/model/model.test.ts | 190 +- .../common/model/modelDecorations.test.ts | 68 +- .../common/model/modelEditOperation.test.ts | 18 +- .../pieceTreeTextBuffer.test.ts | 358 +- .../test/common/model/textChange.test.ts | 4 +- .../test/common/model/textModel.test.ts | 440 +-- .../test/common/model/textModelSearch.test.ts | 30 +- .../common/model/textModelWithTokens.test.ts | 30 +- .../test/common/model/tokensStore.test.ts | 16 +- .../modes/languageConfiguration.test.ts | 92 +- .../common/modes/languageSelector.test.ts | 90 +- .../test/common/modes/linkComputer.test.ts | 4 +- .../modes/supports/characterPair.test.ts | 104 +- .../modes/supports/electricCharacter.test.ts | 6 +- .../modes/supports/javascriptOnEnterRules.ts | 2 +- .../common/modes/supports/onEnter.test.ts | 18 +- .../modes/supports/richEditBrackets.test.ts | 38 +- .../modes/supports/tokenization.test.ts | 46 +- .../common/modes/textToHtmlTokenizer.test.ts | 28 +- .../services/editorSimpleWorker.test.ts | 39 +- .../common/services/languagesRegistry.test.ts | 136 +- .../test/common/services/modelService.test.ts | 165 +- .../services}/semanticTokensDto.test.ts | 6 +- .../testTextResourcePropertiesService.ts | 27 + .../common/view/overviewZoneManager.test.ts | 6 +- .../viewLayout/editorLayoutProvider.test.ts | 5 +- .../common/viewLayout/lineDecorations.test.ts | 22 +- .../common/viewLayout/linesLayout.test.ts | 1666 ++++----- .../viewLayout/viewLineRenderer.test.ts | 82 +- .../monospaceLineBreaksComputer.test.ts | 66 +- .../viewModel/prefixSumComputer.test.ts | 206 +- .../viewModel/splitLinesCollection.test.ts | 235 +- .../viewModel/viewModelDecorations.test.ts | 228 +- .../common/viewModel/viewModelImpl.test.ts | 50 +- .../node/classification/typescript.test.ts | 2 +- src/vs/loader.js | 20 +- src/vs/monaco.d.ts | 149 +- .../browser/menuEntryActionViewItem.css | 14 + .../browser/menuEntryActionViewItem.ts | 167 +- src/vs/platform/actions/common/actions.ts | 98 +- src/vs/platform/actions/common/menuService.ts | 56 +- .../backup/electron-main/backupMainService.ts | 46 +- .../electron-main/backupMainService.test.ts | 284 +- .../common/configurationModels.ts | 5 +- .../common/configurationRegistry.ts | 7 + .../common/configurationService.ts | 3 +- .../contextkey/browser/contextKeyService.ts | 44 +- .../platform/contextkey/common/contextkey.ts | 490 +-- .../contextkey/test/common/contextkey.test.ts | 114 +- .../contextview/browser/contextMenuHandler.ts | 2 +- .../debug/common/extensionHostDebug.ts | 9 - .../debug/common/extensionHostDebugIpc.ts | 16 +- .../electron-main/extensionHostDebugIpc.ts | 3 +- .../diagnostics/node/diagnosticsService.ts | 116 +- src/vs/platform/dialogs/common/dialogs.ts | 1 + .../{dialogs.ts => dialogMainService.ts} | 176 +- src/vs/platform/environment/common/argv.ts | 1 + src/vs/platform/environment/node/argv.ts | 9 +- .../test/node/environmentService.test.ts | 48 +- .../test/node/nativeModules.test.ts | 7 +- .../common/extensionGalleryService.ts | 14 + .../common/extensionManagement.ts | 16 + .../common/extensionManagementCLIService.ts | 347 ++ .../common/extensionManagementUtil.ts | 6 +- .../electron-sandbox/extensionTipsService.ts | 14 + .../node/extensionDownloader.ts | 7 +- .../node/extensionManagementService.ts | 11 +- .../node/extensionsScanner.ts | 19 +- .../common/extensionGalleryService.test.ts | 48 + .../test/node/extensionGalleryService.test.ts | 178 - .../common/extensionRecommendations.ts | 9 + .../platform/extensions/common/extensions.ts | 2 +- .../browser/indexedDBFileSystemProvider.ts | 385 ++- src/vs/platform/files/common/fileService.ts | 157 +- src/vs/platform/files/common/files.ts | 6 +- src/vs/platform/files/common/io.ts | 7 +- .../files/node/diskFileSystemProvider.ts | 28 +- .../node/watcher/nodejs/watcherService.ts | 12 +- .../node/watcher/nsfw/nsfwWatcherService.ts | 24 +- .../nsfw/test/nsfwWatcherService.test.ts | 28 +- .../files/node/watcher/nsfw/watcherService.ts | 6 +- .../watcher/unix/chokidarWatcherService.ts | 9 +- .../unix/test/chockidarWatcherService.test.ts | 6 +- .../files/node/watcher/unix/watcherService.ts | 6 +- .../files/test/browser/fileService.test.ts | 52 +- .../test/browser/indexedDBFileService.test.ts | 344 +- .../electron-browser/diskFileService.test.ts | 700 ++-- .../test/electron-browser/normalizer.test.ts | 12 +- .../common/instantiationService.ts | 21 +- .../electron-browser/sharedProcessService.ts | 56 +- .../electron-main/sharedProcessMainService.ts | 36 - .../electron-sandbox/mainProcessService.ts | 35 +- src/vs/platform/issue/common/issue.ts | 2 +- .../issue/electron-main/issueMainService.ts | 198 +- .../common/abstractKeybindingService.ts | 58 +- .../common/baseResolvedKeybinding.ts | 5 + .../keybinding/common/keybindingResolver.ts | 36 - .../common/resolvedKeybindingItem.ts | 4 + .../common/usLayoutResolvedKeybinding.ts | 16 + .../common/abstractKeybindingService.test.ts | 110 +- .../test/common/keybindingLabels.test.ts | 8 +- .../test/common/keybindingResolver.test.ts | 44 +- src/vs/platform/label/common/label.ts | 4 +- .../launch/electron-main/launchMainService.ts | 86 +- src/vs/platform/lifecycle/common/lifecycle.ts | 4 +- .../electron-main/lifecycleMainService.ts | 6 +- src/vs/platform/list/browser/listService.ts | 97 +- .../localizations/common/localizations.ts | 2 + .../localizations/node/localizations.ts | 15 +- src/vs/platform/log/node/loggerService.ts | 6 +- .../platform/markers/common/markerService.ts | 7 +- .../platform/menubar/electron-main/menubar.ts | 44 +- src/vs/platform/native/common/native.ts | 3 +- .../electron-main/nativeHostMainService.ts | 33 +- src/vs/platform/opener/browser/link.ts | 25 +- src/vs/platform/opener/common/opener.ts | 26 +- src/vs/platform/product/common/product.ts | 16 +- .../platform/product/common/productService.ts | 2 + .../registry/test/common/platform.test.ts | 33 +- .../remote/common/remoteAgentConnection.ts | 272 +- .../remote/common/remoteAgentEnvironment.ts | 2 + src/vs/platform/remote/common/remoteHosts.ts | 2 +- src/vs/platform/remote/common/tunnel.ts | 189 +- .../platform/remote/node/nodeSocketFactory.ts | 2 +- src/vs/platform/remote/node/tunnelService.ts | 33 +- .../sharedProcess/node/sharedProcess.ts | 34 + src/vs/platform/state/node/state.ts | 2 + src/vs/platform/state/node/stateService.ts | 6 +- src/vs/platform/state/test/node/state.test.ts | 43 +- .../storage/browser/storageService.ts | 6 +- src/vs/platform/storage/node/storageIpc.ts | 4 +- .../platform/storage/node/storageService.ts | 34 +- .../test/common/storageService.test.ts | 38 +- .../test/electron-browser/storage.test.ts | 45 +- .../storage/test/node/storageService.test.ts | 47 +- .../{node => common}/commonProperties.ts | 33 +- .../{node => common}/telemetryIpc.ts | 0 .../telemetry/common/telemetryUtils.ts | 3 +- .../platform/theme/browser/iconsStyleSheet.ts | 61 + src/vs/platform/theme/common/colorRegistry.ts | 8 +- src/vs/platform/theme/common/iconRegistry.ts | 112 +- src/vs/platform/theme/common/themeService.ts | 66 +- .../theme/test/common/testThemeService.ts | 9 +- src/vs/platform/undoRedo/common/undoRedo.ts | 8 + .../undoRedo/common/undoRedoService.ts | 88 +- .../test/common/undoRedoService.test.ts | 158 +- .../electron-main/updateService.darwin.ts | 8 +- .../electron-main/updateService.win32.ts | 7 +- .../common/extensionsStorageSync.ts | 3 +- .../userDataSync/common/extensionsSync.ts | 106 +- .../userDataSync/common/userDataSync.ts | 15 +- .../common/userDataSyncBackupStoreService.ts | 5 +- .../common/userDataSyncService.ts | 214 +- .../common/userDataSyncStoreService.ts | 9 +- .../test/common/synchronizer.test.ts | 102 +- .../common/userDataAutoSyncService.test.ts | 32 +- .../test/common/userDataSyncClient.ts | 24 +- .../common/userDataSyncStoreService.test.ts | 6 +- .../platform/webview/common/resourceLoader.ts | 108 +- .../webview/common/webviewManagerService.ts | 8 +- .../webview/common/webviewPortMapping.ts | 10 +- .../electron-main/webviewMainService.ts | 23 +- .../electron-main/webviewProtocolProvider.ts | 129 +- src/vs/platform/windows/common/windows.ts | 123 +- .../platform/windows/electron-main/windows.ts | 39 +- .../windows/electron-main/windowsFinder.ts | 79 + .../electron-main/windowsMainService.ts | 1182 +++---- .../electron-main/windowsStateHandler.ts | 450 +++ .../electron-main/windowsStateStorage.ts | 93 - src/vs/platform/windows/node/window.ts | 150 - .../windows/{common => node}/windowTracker.ts | 0 .../windows/test/electron-main/window.test.ts | 109 + .../electron-main/windowsStateStorage.test.ts | 292 -- .../platform/windows/test/node/window.test.ts | 127 - src/vs/platform/workspace/common/workspace.ts | 85 +- .../workspace/test/common/workspace.test.ts | 237 +- .../platform/workspaces/common/workspaces.ts | 239 +- .../workspacesHistoryMainService.ts | 171 +- .../electron-main/workspacesMainService.ts | 338 +- .../workspacesManagementMainService.ts | 394 +++ .../electron-main/workspacesService.ts | 81 - .../workspaces/test/common/workspaces.test.ts | 33 + .../workspacesHistoryStorage.test.ts | 112 +- ...> workspacesManagementMainService.test.ts} | 268 +- src/vs/vscode.d.ts | 120 +- src/vs/vscode.proposed.d.ts | 1120 +++--- .../api/browser/extensionHost.contribution.ts | 7 + .../api/browser/mainThreadAuthentication.ts | 330 +- .../api/browser/mainThreadBulkEdits.ts | 26 +- .../api/browser/mainThreadCLICommands.ts | 108 + .../api/browser/mainThreadComments.ts | 2 +- .../api/browser/mainThreadConsole.ts | 11 +- .../api/browser/mainThreadCustomEditors.ts | 9 +- .../api/browser/mainThreadDebugService.ts | 6 +- .../api/browser/mainThreadDialogs.ts | 16 +- .../workbench/api/browser/mainThreadEditor.ts | 9 - .../api/browser/mainThreadEditorTabs.ts | 79 + .../api/browser/mainThreadEditors.ts | 2 +- .../api/browser/mainThreadExtensionService.ts | 36 +- .../mainThreadFileSystemEventService.ts | 161 +- .../api/browser/mainThreadLanguageFeatures.ts | 32 +- .../api/browser/mainThreadMessageService.ts | 6 +- .../api/browser/mainThreadNotebook.ts | 46 +- .../api/browser/mainThreadSecretState.ts | 74 + .../workbench/api/browser/mainThreadTask.ts | 12 +- .../api/browser/mainThreadTerminalService.ts | 122 +- .../api/browser/mainThreadTesting.ts | 60 +- .../api/browser/mainThreadTreeViews.ts | 20 +- .../api/browser/mainThreadTunnelService.ts | 80 +- .../api/browser/mainThreadUriOpeners.ts | 132 + .../api/browser/mainThreadWebviewPanels.ts | 8 +- .../workbench/api/browser/mainThreadWindow.ts | 6 +- .../api/browser/mainThreadWorkspace.ts | 24 +- .../api/browser/viewsExtensionPoint.ts | 76 +- src/vs/workbench/api/common/apiCommands.ts | 9 +- .../workbench/api/common/exHostSecretState.ts | 38 + .../workbench/api/common/extHost.api.impl.ts | 131 +- .../api/common/extHost.common.services.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 142 +- .../api/common/extHostApiCommands.ts | 67 +- .../api/common/extHostAuthentication.ts | 141 +- .../workbench/api/common/extHostClipboard.ts | 24 +- .../workbench/api/common/extHostCodeInsets.ts | 6 +- .../workbench/api/common/extHostCommands.ts | 11 +- .../api/common/extHostCustomEditors.ts | 12 +- .../api/common/extHostDebugService.ts | 26 +- .../api/common/extHostDocumentsAndEditors.ts | 23 +- .../workbench/api/common/extHostEditorTabs.ts | 39 + .../api/common/extHostExtensionService.ts | 37 +- .../api/common/extHostFileSystemConsumer.ts | 74 +- .../common/extHostFileSystemEventService.ts | 48 +- .../api/common/extHostLanguageFeatures.ts | 55 +- .../api/common/extHostMessageService.ts | 6 + .../workbench/api/common/extHostNotebook.ts | 108 +- .../api/common/extHostNotebookDocument.ts | 45 +- .../api/common/extHostNotebookEditor.ts | 2 +- .../workbench/api/common/extHostQuickOpen.ts | 8 +- .../api/common/extHostRequireInterceptor.ts | 3 + src/vs/workbench/api/common/extHostSecrets.ts | 43 + .../workbench/api/common/extHostStatusBar.ts | 45 +- .../api/common/extHostStoragePaths.ts | 6 +- src/vs/workbench/api/common/extHostTask.ts | 7 +- .../api/common/extHostTerminalService.ts | 302 +- src/vs/workbench/api/common/extHostTesting.ts | 462 +-- .../workbench/api/common/extHostTextEditor.ts | 451 ++- .../api/common/extHostTextEditors.ts | 25 +- .../workbench/api/common/extHostTreeViews.ts | 24 +- .../api/common/extHostTunnelService.ts | 24 +- .../api/common/extHostTypeConverters.ts | 33 +- src/vs/workbench/api/common/extHostTypes.ts | 98 +- .../workbench/api/common/extHostUriOpener.ts | 74 + .../api/common/extHostWebviewPanels.ts | 4 +- .../workbench/api/common/extHostWorkspace.ts | 36 +- .../api/common/shared/workspaceContains.ts | 3 +- src/vs/workbench/api/node/extHostCLIServer.ts | 36 +- .../workbench/api/node/extHostDebugService.ts | 32 +- .../api/node/extHostExtensionService.ts | 15 +- .../api/node/extHostOutputService.ts | 7 +- src/vs/workbench/api/node/extHostTask.ts | 5 +- .../api/node/extHostTerminalService.ts | 25 +- .../api/node/extHostTunnelService.ts | 284 +- .../api/worker/extHostExtensionService.ts | 68 +- .../browser/actions/layoutActions.ts | 238 +- .../workbench/browser/actions/listCommands.ts | 20 +- .../browser/actions/navigationActions.ts | 17 +- .../browser/actions/textInputActions.ts | 29 +- .../browser/actions/windowActions.ts | 38 +- .../browser/actions/workspaceActions.ts | 6 +- .../browser/actions/workspaceCommands.ts | 10 +- .../editor/editorWidgets.ts => codeeditor.ts} | 114 +- src/vs/workbench/browser/composite.ts | 16 +- src/vs/workbench/browser/contextkeys.ts | 16 +- src/vs/workbench/browser/dnd.ts | 4 +- src/vs/workbench/browser/editor.ts | 1 - src/vs/workbench/browser/labels.ts | 46 +- src/vs/workbench/browser/layout.ts | 84 +- src/vs/workbench/browser/menuActions.ts | 99 + src/vs/workbench/browser/panecomposite.ts | 30 +- src/vs/workbench/browser/panel.ts | 63 +- src/vs/workbench/browser/part.ts | 4 +- .../parts/activitybar/activitybarActions.ts | 121 +- .../parts/activitybar/activitybarPart.ts | 314 +- .../workbench/browser/parts/compositeBar.ts | 38 +- .../browser/parts/compositeBarActions.ts | 29 +- .../workbench/browser/parts/compositePart.ts | 20 +- .../parts/dialogs/dialog.web.contribution.ts | 4 +- .../browser/parts/dialogs/dialogHandler.ts | 2 +- .../browser/parts/editor/binaryEditor.ts | 2 +- .../parts/editor/breadcrumbsControl.ts | 321 +- .../browser/parts/editor/breadcrumbsModel.ts | 260 +- .../browser/parts/editor/breadcrumbsPicker.ts | 283 +- .../parts/editor/editor.contribution.ts | 10 +- .../browser/parts/editor/editorActions.ts | 108 +- .../browser/parts/editor/editorCommands.ts | 24 +- .../browser/parts/editor/editorDropTarget.ts | 3 +- .../browser/parts/editor/editorGroupView.ts | 34 +- .../browser/parts/editor/editorPart.ts | 4 +- .../browser/parts/editor/editorStatus.ts | 159 +- .../parts/editor/media/notabstitlecontrol.css | 37 +- .../parts/editor/media/tabstitlecontrol.css | 126 +- .../parts/editor/media/titlecontrol.css | 14 +- .../parts/editor/noTabsTitleControl.ts | 20 +- .../browser/parts/editor/sideBySideEditor.ts | 4 +- .../browser/parts/editor/tabsTitleControl.ts | 331 +- .../browser/parts/editor/textDiffEditor.ts | 8 +- .../browser/parts/editor/textEditor.ts | 1 - .../browser/parts/editor/titleControl.ts | 83 +- .../notifications/notificationsCenter.ts | 4 +- .../parts/notifications/notificationsList.ts | 4 +- .../notifications/notificationsViewer.ts | 4 +- .../browser/parts/panel/panelActions.ts | 190 +- .../browser/parts/panel/panelPart.ts | 94 +- .../parts/sidebar/media/sidebarpart.css | 5 - .../browser/parts/sidebar/sidebarPart.ts | 8 +- .../browser/parts/statusbar/statusbarPart.ts | 34 +- .../parts/titlebar/media/titlebarpart.css | 25 +- .../browser/parts/titlebar/menubarControl.ts | 132 +- .../browser/parts/titlebar/titlebarPart.ts | 48 +- .../browser/parts/views/media/views.css | 6 + .../workbench/browser/parts/views/treeView.ts | 62 +- .../browser/parts/views/viewMenuActions.ts | 105 - .../workbench/browser/parts/views/viewPane.ts | 642 ++++ .../browser/parts/views/viewPaneContainer.ts | 749 +--- .../browser/parts/views/viewsService.ts | 298 +- .../browser/parts/views/viewsViewlet.ts | 19 +- src/vs/workbench/browser/style.ts | 4 +- src/vs/workbench/browser/viewlet.ts | 100 +- src/vs/workbench/browser/web.main.ts | 111 +- src/vs/workbench/browser/window.ts | 161 + .../browser/workbench.contribution.ts | 54 +- src/vs/workbench/browser/workbench.ts | 30 +- src/vs/workbench/common/actions.ts | 1 + src/vs/workbench/common/composite.ts | 5 + src/vs/workbench/common/dialogs.ts | 2 + src/vs/workbench/common/editor.ts | 22 +- .../common/editor/diffEditorInput.ts | 8 +- .../common/editor/diffEditorModel.ts | 12 +- src/vs/workbench/common/editor/editorGroup.ts | 7 + .../common/editor/resourceEditorInput.ts | 2 +- .../common/editor/resourceEditorModel.ts | 2 +- .../common/editor/textDiffEditorModel.ts | 14 +- .../common/editor/textEditorModel.ts | 6 +- .../common/editor/textResourceEditorInput.ts | 15 +- src/vs/workbench/common/panel.ts | 2 + src/vs/workbench/common/theme.ts | 4 +- src/vs/workbench/common/views.ts | 140 +- .../contrib/backup/common/backupRestorer.ts | 17 +- .../contrib/backup/common/backupTracker.ts | 4 +- .../backup/electron-sandbox/backupTracker.ts | 173 +- .../backupRestorer.test.ts | 97 +- .../backup/test/browser/backupTracker.test.ts | 158 + .../electron-browser/backupTracker.test.ts | 302 +- .../bulkEdit/browser/bulkEditService.ts | 30 +- .../contrib/bulkEdit/browser/bulkFileEdits.ts | 301 +- .../browser/preview/bulkEdit.contribution.ts | 2 +- .../bulkEdit/browser/preview/bulkEditPane.ts | 4 +- .../bulkEdit/browser/preview/bulkEditTree.ts | 16 +- .../browser/callHierarchyPeek.ts | 8 - .../contrib/cli/node/cli.contribution.ts | 11 +- .../browser/accessibility/accessibility.ts | 2 +- .../codeEditor/browser/diffEditorHelper.ts | 8 +- .../languageConfigurationExtensionPoint.ts | 289 +- .../browser/largeFileOptimizations.ts | 2 +- .../browser/outline/documentSymbolsOutline.ts | 429 +++ .../browser/outline/documentSymbolsTree.css} | 13 +- .../browser/outline/documentSymbolsTree.ts | 305 ++ .../quickaccess/gotoLineQuickAccess.ts | 8 +- .../quickaccess/gotoSymbolQuickAccess.ts | 134 +- .../codeEditor/browser/toggleWordWrap.ts | 140 +- .../test/browser/saveParticipant.test.ts | 28 +- .../comments/browser/commentThreadWidget.ts | 13 +- .../comments/browser/commentsTreeViewer.ts | 1 + .../contrib/comments/browser/commentsView.ts | 58 +- .../contrib/comments/browser/media/review.css | 2 +- .../browser/customEditor.contribution.ts | 6 +- .../customEditor/browser/customEditorInput.ts | 18 - .../browser/customEditorInputFactory.ts | 36 +- .../customEditor/browser/customEditors.ts | 26 +- .../browser/breakpointEditorContribution.ts | 42 +- .../contrib/debug/browser/breakpointWidget.ts | 3 + .../contrib/debug/browser/breakpointsView.ts | 650 +++- .../browser/callStackEditorContribution.ts | 20 +- .../contrib/debug/browser/callStackView.ts | 97 +- .../debug/browser/debug.contribution.ts | 193 +- .../debug/browser/debugActionViewItems.ts | 8 +- .../contrib/debug/browser/debugActions.ts | 421 --- .../debug/browser/debugAdapterManager.ts | 4 +- .../contrib/debug/browser/debugColors.ts | 128 +- .../contrib/debug/browser/debugCommands.ts | 104 +- .../debug/browser/debugEditorActions.ts | 90 +- .../debug/browser/debugEditorContribution.ts | 10 +- .../contrib/debug/browser/debugHover.ts | 2 +- .../contrib/debug/browser/debugIcons.ts | 48 +- .../contrib/debug/browser/debugQuickAccess.ts | 3 +- .../contrib/debug/browser/debugService.ts | 57 +- .../contrib/debug/browser/debugSession.ts | 24 +- .../contrib/debug/browser/debugTaskRunner.ts | 19 +- .../contrib/debug/browser/debugToolBar.ts | 219 +- .../contrib/debug/browser/debugViewlet.ts | 249 +- .../contrib/debug/browser/exceptionWidget.ts | 4 + .../contrib/debug/browser/linkDetector.ts | 52 +- .../debug/browser/loadedScriptsView.ts | 2 +- .../debug/browser/media/debugViewlet.css | 14 +- .../contrib/debug/browser/media/repl.css | 3 + .../contrib/debug/browser/rawDebugSession.ts | 7 +- .../workbench/contrib/debug/browser/repl.ts | 349 +- .../contrib/debug/browser/replFilter.ts | 2 +- .../contrib/debug/browser/replViewer.ts | 33 +- .../contrib/debug/browser/variablesView.ts | 93 +- .../debug/browser/watchExpressionsView.ts | 165 +- .../contrib/debug/browser/welcomeView.ts | 12 +- .../workbench/contrib/debug/common/debug.ts | 28 +- .../contrib/debug/common/debugModel.ts | 24 +- .../contrib/debug/common/debugProtocol.d.ts | 11 +- .../contrib/debug/common/debugStorage.ts | 9 + .../contrib/debug/common/debugViewModel.ts | 22 +- .../contrib/debug/common/debugger.ts | 4 +- .../contrib/debug/common/replModel.ts | 22 +- .../contrib/debug/node/debugHelperService.ts | 6 +- .../contrib/debug/node/telemetryApp.ts | 2 +- .../workbench/contrib/debug/node/terminals.ts | 6 +- .../debug/test/browser/breakpoints.test.ts | 4 +- .../debug/test/browser/callStack.test.ts | 48 +- .../debugANSIHandling.test.ts | 0 .../debug/test/browser/linkDetector.test.ts | 10 +- .../contrib/debug/test/browser/mockDebug.ts | 126 +- .../contrib/debug/test/browser/repl.test.ts | 28 +- .../debug/test/browser/telemetry.test.ts | 2 +- .../contrib/debug/test/node/debugger.test.ts | 2 +- .../abstractRuntimeExtensionsEditor.ts | 35 +- .../extensions/browser/extensionEditor.ts | 63 +- ...ensionRecommendationNotificationService.ts | 24 +- .../browser/extensions.contribution.ts | 1501 ++++---- .../extensions/browser/extensionsActions.ts | 653 +--- .../browser/extensionsDependencyChecker.ts | 3 +- .../extensions/browser/extensionsList.ts | 3 +- .../extensions/browser/extensionsViewlet.ts | 166 +- .../extensions/browser/extensionsViews.ts | 128 +- .../browser/extensionsWorkbenchService.ts | 26 +- .../extensions/browser/media/extension.css | 4 + .../browser/remoteExtensionsInstaller.ts | 59 - .../contrib/extensions/common/extensions.ts | 15 +- .../extensions.contribution.ts | 4 +- .../extensionsAutoProfiler.ts | 10 +- .../reportExtensionIssueAction.ts | 8 +- .../extensionRecommendationsService.test.ts | 51 +- .../extensionsActions.test.ts | 82 +- .../electron-browser/extensionsViews.test.ts | 5 +- .../extensionsWorkbenchService.test.ts | 5 +- .../externalUriOpener/common/configuration.ts | 73 + .../common/contributedOpeners.ts | 114 + .../common/externalUriOpener.contribution.ts | 15 + .../common/externalUriOpenerService.ts | 241 ++ .../common/externalUriOpenerService.test.ts | 128 + .../files/browser/editors/binaryFileEditor.ts | 10 +- .../files/browser/editors/textFileEditor.ts | 26 +- .../editors/textFileSaveErrorHandler.ts | 14 +- .../contrib/files/browser/explorerService.ts | 106 +- .../contrib/files/browser/explorerViewlet.ts | 20 +- .../files/browser/fileActions.contribution.ts | 19 +- .../contrib/files/browser/fileActions.ts | 183 +- .../contrib/files/browser/fileCommands.ts | 40 +- .../files/browser/files.contribution.ts | 119 +- .../workbench/contrib/files/browser/files.ts | 61 +- .../files/browser/media/explorerviewlet.css | 2 +- .../contrib/files/browser/views/emptyView.ts | 2 +- .../files/browser/views/explorerView.ts | 147 +- .../files/browser/views/explorerViewer.ts | 79 +- .../files/browser/views/openEditorsView.ts | 137 +- .../contrib/files/common/workspaceWatcher.ts | 2 +- .../files/electron-sandbox/textFileEditor.ts | 28 +- .../files/test/browser/editorAutoSave.test.ts | 51 +- .../files/test/browser/fileActions.test.ts | 32 +- .../test/browser/fileEditorInput.test.ts | 68 +- .../test/browser/fileOnDiskProvider.test.ts | 6 +- .../files/test/browser/textFileEditor.test.ts | 112 - .../browser/textFileEditorTracker.test.ts | 70 +- .../format/browser/formatActionsMultiple.ts | 31 +- .../issue/electron-sandbox/issueService.ts | 14 +- .../browser/localizations.contribution.ts | 2 +- .../contrib/logs/common/logsDataCleaner.ts | 3 +- .../markers/browser/markers.contribution.ts | 261 +- .../contrib/markers/browser/markers.ts | 20 + .../contrib/markers/browser/markersModel.ts | 1 + .../markers/browser/markersTreeViewer.ts | 30 +- .../contrib/markers/browser/markersView.ts | 107 +- .../markers/browser/markersViewActions.ts | 58 +- .../contrib/markers/browser/messages.ts | 4 +- .../contrib/notebook/browser/constants.ts | 4 +- .../notebook/browser/contrib/coreActions.ts | 241 +- .../browser/contrib/find/findController.ts | 15 +- .../contrib/outline/notebookOutline.css | 45 + .../contrib/outline/notebookOutline.ts | 607 ++++ .../notebook/browser/contrib/scm/scm.ts | 163 - .../browser/contrib/status/editorStatus.ts | 58 +- .../browser/contrib/toc/tocProvider.ts | 72 - .../notebook/browser/diff/cellComponents.ts | 1091 ------ .../browser/diff/celllDiffViewModel.ts | 48 - .../contrib/notebook/browser/diff/common.ts | 31 - .../notebook/browser/diff/diffComponents.ts | 1463 ++++++++ .../browser/diff/diffElementOutputs.ts | 361 ++ .../browser/diff/diffElementViewModel.ts | 498 +++ .../browser/diff/diffNestedCellViewModel.ts | 123 + .../notebook/browser/diff/eventDispatcher.ts | 58 + .../notebook/browser/diff/notebookDiff.css | 155 +- .../browser/diff/notebookDiffActions.ts | 85 +- .../browser/diff/notebookDiffEditorBrowser.ts | 117 + .../browser/diff/notebookTextDiffEditor.ts | 489 ++- .../browser/diff/notebookTextDiffList.ts | 270 +- .../notebook/browser/media/notebook.css | 53 +- .../notebook/browser/notebook.contribution.ts | 235 +- .../notebook/browser/notebookBrowser.ts | 146 +- .../browser/notebookDiffEditorInput.ts | 21 +- .../notebook/browser/notebookEditor.ts | 29 +- .../notebook/browser/notebookEditorInput.ts | 18 - .../notebook/browser/notebookEditorWidget.ts | 249 +- .../browser/notebookEditorWidgetService.ts | 143 +- .../notebookEditorWidgetServiceImpl.ts | 149 + .../contrib/notebook/browser/notebookIcons.ts | 15 +- .../notebook/browser/notebookRegistry.ts | 6 +- .../notebook/browser/notebookServiceImpl.ts | 77 +- .../notebook/browser/view/notebookCellList.ts | 134 +- .../browser/view/output/outputRenderer.ts | 15 +- .../view/output/transforms/errorTransform.ts | 11 +- .../view/output/transforms/richTransform.ts | 70 +- .../view/output/transforms/streamTransform.ts | 13 +- .../view/output/transforms/textHelper.ts | 41 +- .../view/renderers/backLayerWebView.ts | 164 +- .../browser/view/renderers/cellActionView.ts | 4 +- .../browser/view/renderers/cellContextKeys.ts | 7 +- .../browser/view/renderers/cellOutput.ts | 277 +- .../browser/view/renderers/cellRenderer.ts | 46 +- .../browser/view/renderers/cellWidgets.ts | 13 +- .../browser/view/renderers/codeCell.ts | 15 +- .../browser/view/renderers/markdownCell.ts | 18 +- .../browser/view/renderers/webviewPreloads.ts | 38 +- .../view/renderers/webviewThemeMapping.ts | 71 + .../browser/viewModel/cellOutputViewModel.ts | 57 + .../browser/viewModel/codeCellViewModel.ts | 30 +- .../browser/viewModel/eventDispatcher.ts | 20 - .../viewModel/markdownCellViewModel.ts | 23 +- .../common/model/notebookCellTextModel.ts | 10 +- .../common/model/notebookTextModel.ts | 59 +- .../contrib/notebook/common/notebookCommon.ts | 133 +- .../notebook/common/notebookEditorModel.ts | 65 +- .../notebookEditorModelResolverService.ts | 20 +- .../notebook/common/notebookService.ts | 5 +- .../common/services/notebookSimpleWorker.ts | 2 +- .../electron-browser/notebook.contribution.ts | 2 +- .../notebook/test/notebookTextModel.test.ts | 44 +- .../notebook/test/notebookViewModel.test.ts | 6 +- .../notebook/test/testNotebookEditor.ts | 31 +- .../outline/browser/outline.contribution.ts | 13 +- .../contrib/outline/browser/outlinePane.ts | 824 ++--- .../outline/browser/outlineViewState.ts | 87 + .../output/browser/output.contribution.ts | 79 +- .../contrib/output/browser/outputView.ts | 2 +- .../output/common/outputChannelModel.ts | 2 +- .../test/browser/outputLinkProvider.test.ts | 268 +- .../performance/browser/perfviewEditor.ts | 78 +- .../electron-browser/startupProfiler.ts | 9 +- .../electron-browser/startupTimings.ts | 9 +- .../preferences/browser/keybindingsEditor.ts | 32 +- .../browser/keybindingsEditorContribution.ts | 2 +- .../browser/preferences.contribution.ts | 25 +- .../preferences/browser/preferencesEditor.ts | 2 +- .../preferences/browser/preferencesIcons.ts | 18 +- .../browser/preferencesRenderers.ts | 2 +- .../preferences/browser/settingsEditor2.ts | 4 +- .../preferences/browser/settingsLayout.ts | 2 +- .../preferences/browser/settingsTree.ts | 36 +- .../preferences/browser/settingsTreeModels.ts | 11 +- .../contrib/preferences/common/preferences.ts | 1 + .../browser/commandsQuickAccess.ts | 5 +- .../browser/relauncher.contribution.ts | 14 - .../remote/browser/explorerViewItems.ts | 57 +- .../contrib/remote/browser/remote.ts | 314 +- .../contrib/remote/browser/remoteExplorer.ts | 342 +- .../contrib/remote/browser/remoteIcons.ts | 26 + .../contrib/remote/browser/remoteIndicator.ts | 94 +- .../contrib/remote/browser/tunnelView.ts | 328 +- .../contrib/remote/browser/urlFinder.ts | 4 +- .../remote/common/remote.contribution.ts | 44 +- .../contrib/remote/common/showCandidate.ts | 6 +- .../contrib/remote/common/tunnelFactory.ts | 59 +- .../electron-sandbox/remote.contribution.ts | 6 +- .../workbench/contrib/scm/browser/activity.ts | 9 +- .../contrib/scm/browser/media/scm.css | 5 +- .../contrib/scm/browser/scm.contribution.ts | 51 +- .../scm/browser/scmRepositoriesViewPane.ts | 15 +- .../contrib/scm/browser/scmViewPane.ts | 724 ++-- .../scm/browser/scmViewPaneContainer.ts | 13 - src/vs/workbench/contrib/scm/browser/util.ts | 35 +- src/vs/workbench/contrib/scm/common/scm.ts | 1 - .../search/browser/anythingQuickAccess.ts | 23 +- .../search/browser/media/searchEditor.css | 14 +- .../search/browser/media/searchview.css | 12 +- .../search/browser/patternInputWidget.ts | 67 +- .../contrib/search/browser/replaceService.ts | 3 +- .../search/browser/search.contribution.ts | 328 +- .../contrib/search/browser/searchActions.ts | 354 +- .../contrib/search/browser/searchIcons.ts | 4 +- .../contrib/search/browser/searchView.ts | 307 +- .../contrib/search/browser/searchWidget.ts | 6 +- .../search/browser/symbolsQuickAccess.ts | 12 +- .../contrib/search/common/constants.ts | 5 +- .../contrib/search/common/queryBuilder.ts | 153 +- .../workbench/contrib/search/common/search.ts | 9 + .../contrib/search/common/searchModel.ts | 2 +- .../search/test/browser/queryBuilder.test.ts | 72 +- .../search/test/common/cacheState.test.ts | 2 +- .../search/test/common/searchModel.test.ts | 3 +- .../browser/media/searchEditor.css | 14 +- .../browser/searchEditor.contribution.ts | 65 +- .../searchEditor/browser/searchEditor.ts | 34 +- .../browser/searchEditorActions.ts | 71 +- .../searchEditor/browser/searchEditorInput.ts | 28 +- .../browser/searchEditorSerialization.ts | 4 + .../contrib/snippets/browser/insertSnippet.ts | 2 +- .../browser/snippetCompletionProvider.ts | 24 +- .../snippets/browser/snippetsService.ts | 7 + .../snippets/test/browser/snippetFile.test.ts | 18 +- .../test/browser/snippetsRegistry.test.ts | 2 +- .../test/browser/snippetsRewrite.test.ts | 10 +- .../test/browser/snippetsService.test.ts | 175 +- .../partsSplash.contribution.ts | 2 +- .../contrib/tags/common/javaWorkspaceTags.ts | 48 + .../electron-browser/workspaceTagsService.ts | 65 +- .../tasks/browser/abstractTaskService.ts | 12 +- .../tasks/browser/task.contribution.ts | 8 +- .../contrib/tasks/browser/taskQuickPick.ts | 2 +- .../tasks/browser/terminalTaskSystem.ts | 12 +- .../contrib/tasks/common/jsonSchema_v2.ts | 16 +- .../contrib/tasks/common/problemMatcher.ts | 2 +- .../tasks/common/taskDefinitionRegistry.ts | 7 + .../workbench/contrib/tasks/common/tasks.ts | 3 +- .../tasks/electron-browser/taskService.ts | 2 +- .../tasks/test/common/configuration.test.ts | 5 +- .../browser/links/terminalLinkManager.ts | 38 +- .../terminalValidatedLocalLinkProvider.ts | 7 +- .../terminal/browser/media/terminal.css | 1 + .../terminal/browser/remoteTerminalService.ts | 33 +- .../terminal/browser/terminal.contribution.ts | 26 +- .../contrib/terminal/browser/terminal.ts | 16 +- .../browser/terminal.web.contribution.ts | 4 +- .../terminal/browser/terminalActions.ts | 40 +- .../terminal/browser/terminalInstance.ts | 103 +- .../browser/terminalProcessExtHostProxy.ts | 6 + .../browser/terminalProcessManager.ts | 115 +- .../terminal/browser/terminalService.ts | 125 +- .../contrib/terminal/browser/terminalTab.ts | 61 +- .../browser/terminalTypeAheadAddon.ts | 194 +- .../contrib/terminal/browser/terminalView.ts | 2 +- .../terminal/common/remoteTerminalChannel.ts | 52 +- .../contrib/terminal/common/terminal.ts | 84 +- .../terminal/common/terminalConfiguration.ts | 43 +- .../contrib/terminal/common/terminalMenu.ts | 11 - .../electron-browser/terminal.contribution.ts | 5 +- .../terminalInstanceService.ts | 6 +- .../electron-browser/windowsShellHelper.ts | 9 +- .../contrib/terminal/node/terminal.ts | 59 +- .../terminal/node/terminalEnvironment.ts | 9 +- .../contrib/terminal/node/terminalProcess.ts | 76 +- .../test/browser/terminalConfigHelper.test.ts | 94 +- .../test/browser/terminalTypeahead.test.ts | 133 +- .../terminalEnvironment.test.ts | 10 +- .../hierarchalByLocation.ts | 176 + .../explorerProjections/hierarchalByName.ts | 160 + .../explorerProjections/hierarchalNodes.ts | 146 + .../browser/explorerProjections/index.ts | 93 + .../explorerProjections/locationStore.ts | 80 + .../browser/explorerProjections/nodeHelper.ts | 147 + .../explorerProjections/stateByLocation.ts | 321 ++ .../explorerProjections/stateByName.ts | 289 ++ .../browser/explorerProjections/stateNodes.ts | 35 + .../contrib/testing/browser/icons.ts | 49 + .../contrib/testing/browser/media/testing.css | 107 + .../testing/browser/testExplorerActions.ts | 454 +++ .../testing/browser/testing.contribution.ts | 112 + .../testing/browser/testingDecorations.ts | 326 ++ .../testing/browser/testingExplorerFilter.ts | 179 + .../testing/browser/testingExplorerView.ts | 841 +++++ .../testing/browser/testingOutputPeek.ts | 374 ++ .../browser/testingViewPaneContainer.ts | 49 + .../contrib/testing/browser/theme.ts | 132 + .../contrib/testing/common/constants.ts | 36 + .../testing/common/ownedTestCollection.ts | 230 ++ .../contrib/testing/common/storedValue.ts | 79 + .../contrib/testing/common/testCollection.ts | 60 +- .../testing/common/testResultService.ts | 245 ++ .../contrib/testing/common/testService.ts | 93 +- .../contrib/testing/common/testServiceImpl.ts | 302 +- .../contrib/testing/common/testStubs.ts | 28 + .../testing/common/testingContentProvider.ts | 73 + .../testing/common/testingContextKeys.ts | 18 + .../contrib/testing/common/testingStates.ts | 41 + .../contrib/testing/common/testingUri.ts | 123 + .../common/workspaceTestCollectionService.ts | 230 ++ .../hierarchalByLocation.test.ts | 93 + .../hierarchalByName.test.ts | 129 + .../stateByLocation.test.ts | 154 + .../explorerProjections/stateByName.test.ts | 122 + .../testing/test/browser/testObjectTree.ts | 104 + .../test/common/ownedTestCollection.ts | 37 + .../testing/test/common/testingUri.test.ts | 25 + .../themes/browser/themes.contribution.ts | 23 +- .../contrib/timeline/browser/timelinePane.ts | 18 +- .../update/browser/releaseNotesEditor.ts | 42 +- .../contrib/update/browser/update.ts | 44 +- .../url/browser/trustedDomainsValidator.ts | 92 +- .../workbench/contrib/url/common/urlGlob.ts | 88 + .../browser/userDataSync.contribution.ts | 27 - .../userDataSync/browser/userDataSync.ts | 82 +- .../browser/userDataSyncMergesView.ts | 2 +- .../browser/userDataSyncTrigger.ts | 2 +- .../userDataSync/browser/userDataSyncViews.ts | 52 +- .../contrib/watermark/browser/watermark.ts | 5 - .../webview/browser/baseWebviewElement.ts | 9 +- .../browser/dynamicWebviewEditorOverlay.ts | 15 +- .../contrib/webview/browser/pre/host.js | 83 +- .../contrib/webview/browser/pre/main.js | 7 +- .../webview/browser/pre/service-worker.js | 155 +- .../contrib/webview/browser/themeing.ts | 3 +- .../contrib/webview/browser/webview.ts | 7 +- .../contrib/webview/browser/webviewElement.ts | 98 +- .../electron-browser/pre/electron-index.js | 12 - .../electron-browser/webviewElement.ts | 16 +- .../electron-sandbox/iframeWebviewElement.ts | 2 +- .../electron-sandbox/resourceLoading.ts | 33 +- .../webviewPanel/browser/webviewCommands.ts | 4 +- .../webviewView/browser/webviewViewPane.ts | 2 +- .../common/viewsWelcomeExtensionPoint.ts | 2 +- .../browser/gettingStarted.contribution.ts | 9 +- .../gettingStarted/browser/gettingStarted.css | 260 +- .../gettingStarted/browser/gettingStarted.ts | 330 +- .../browser/gettingStartedIcons.ts | 10 + .../browser/vs_code_editor_getting_started.ts | 39 +- .../page/browser/welcomePage.contribution.ts | 4 +- .../welcome/page/browser/welcomePage.ts | 31 +- .../welcome/page/browser/welcomePageColors.ts | 12 +- .../editor/vs_code_editor_walkthrough.ts | 13 +- .../walkThrough/browser/walkThroughInput.ts | 10 +- .../walkThrough/browser/walkThroughPart.ts | 19 +- .../common/walkThroughContentProvider.ts | 2 +- .../actions/developerActions.ts | 28 - .../electron-browser/desktop.main.ts | 128 +- .../actions/developerActions.ts | 107 +- .../electron-sandbox/actions/windowActions.ts | 38 +- .../electron-sandbox/desktop.contribution.ts | 150 +- .../electron-sandbox/desktop.main.ts | 86 +- .../parts/dialogs/dialog.contribution.ts | 2 +- .../parts/dialogs/dialogHandler.ts | 12 +- .../parts/titlebar/menubarControl.ts | 216 ++ .../parts/titlebar/titlebarPart.ts | 11 +- .../sandbox.simpleservices.ts | 46 +- src/vs/workbench/electron-sandbox/window.ts | 309 +- .../browser/authenticationService.ts | 298 +- .../backupFileService.test.ts | 428 +-- .../clipboard/browser/clipboardService.ts | 59 +- .../configuration/browser/configuration.ts | 101 +- .../browser/configurationService.ts | 161 +- .../configuration/common/configuration.ts | 11 + .../common/configurationEditingService.ts | 11 +- .../configurationCache.ts | 53 +- .../configurationEditingService.test.ts | 395 +++ .../test/browser/configurationService.test.ts | 1985 +++++++++++ .../configurationEditingService.test.ts | 398 --- .../configurationService.test.ts | 2114 ------------ .../electron-sandbox/contextmenuService.ts | 8 +- .../credentials/browser/credentialsService.ts | 8 +- .../credentials/common/credentials.ts | 7 +- .../electron-sandbox/credentialsService.ts | 4 +- .../browser/abstractFileDialogService.ts | 10 +- .../dialogs/browser/simpleFileDialog.ts | 21 +- .../editor/browser/codeEditorService.ts | 10 +- .../services/editor/browser/editorService.ts | 37 +- .../services/editor/common/editorOpenWith.ts | 159 +- .../services/editor/common/editorService.ts | 7 +- .../test/browser/editorGroupsService.test.ts | 693 ++-- .../editor/test/browser/editorService.test.ts | 374 +- .../test/browser/editorsObserver.test.ts | 504 ++- .../browser/extensionBisect.ts | 71 +- .../browser/extensionEnablementService.ts | 27 +- .../common/extensionManagement.ts | 4 +- .../common/extensionManagementService.ts | 71 +- .../common/webExtensionsScannerService.ts | 4 +- .../remoteExtensionManagementService.ts | 5 +- .../extensionEnablementService.test.ts | 48 +- .../common/workspaceExtensionsConfig.ts | 6 +- .../extensions/browser/extensionService.ts | 7 +- .../browser/webWorkerExtensionHost.ts | 58 +- .../common/abstractExtensionService.ts | 15 +- .../extensions/common/extensionHostMain.ts | 21 +- .../extensions/common/extensionHostManager.ts | 7 +- .../common/extensionHostProtocol.ts | 4 +- .../extensions/common/extensionsRegistry.ts | 10 + .../common/polyfillNestedWorker.protocol.ts | 18 + .../extensions/common/remoteExtensionHost.ts | 9 +- .../services/extensions/common/rpcProtocol.ts | 14 +- .../cachedExtensionScanner.ts | 11 +- .../electron-browser/extensionService.ts | 21 + .../localProcessExtensionHost.ts | 79 +- .../node/extensionHostProcessSetup.ts | 34 +- .../extensions/node/extensionPoints.ts | 32 +- .../services/extensions/node/proxyResolver.ts | 7 +- .../test/browser/extensionService.test.ts | 88 + .../test/common/extensionsUtil.test.ts | 2 +- .../test/common/rpcProtocol.test.ts | 32 +- .../extensions/worker/extensionHostWorker.ts | 29 +- .../httpWebWorkerExtensionHostIframe.html | 28 +- .../httpsWebWorkerExtensionHostIframe.html | 28 +- .../extensions/worker/polyfillNestedWorker.ts | 133 + .../common/gettingStartedContent.ts | 268 +- .../common/gettingStartedRegistry.ts | 35 +- .../common/gettingStartedService.ts | 20 +- .../common/media/ColorTheme.jpg | Bin 101006 -> 0 bytes .../common/media/Extensions.jpg | Bin 103983 -> 0 bytes .../gettingStarted/common/media/Languages.jpg | Bin 101550 -> 0 bytes .../common/media/OpenFolder.jpg | Bin 103714 -> 0 bytes .../common/media/colorTheme.png | Bin 0 -> 48327 bytes .../common/media/commandPalette.png | Bin 0 -> 32283 bytes .../common/media/dark/settings.png | Bin 0 -> 25343 bytes .../common/media/extensions.png | Bin 0 -> 77651 bytes .../common/media/forwardPorts.png | Bin 0 -> 31517 bytes .../gettingStarted/common/media/github.png | Bin 0 -> 38564 bytes .../common/media/hc/settings.png | Bin 0 -> 36444 bytes .../common/media/languageExtensions.png | Bin 0 -> 85147 bytes .../common/media/light/settings.png | Bin 0 -> 36898 bytes .../common/media/openFolder.png | Bin 0 -> 35888 bytes .../gettingStarted/common/media/openVSC.png | Bin 0 -> 16170 bytes .../common/media/pullRequests.png | Bin 0 -> 44364 bytes .../common/media/remoteTerminal.png | Bin 0 -> 42347 bytes .../common/media/runProject.png | Bin 0 -> 100705 bytes .../common/media/settingsSync.png | Bin 0 -> 36021 bytes .../gettingStarted/common/media/terminal.png | Bin 0 -> 25762 bytes .../common/media/tutorialVideo.png | Bin 0 -> 51891 bytes .../services/history/browser/history.ts | 181 +- .../services/history/common/history.ts | 10 +- .../history/test/browser/history.test.ts | 101 +- .../host/browser/browserHostService.ts | 6 +- .../services/hover/browser/hoverWidget.ts | 42 +- .../keybinding/browser/keybindingService.ts | 15 +- .../keybinding/browser/unboundCommands.ts | 52 + .../common/macLinuxKeyboardMapper.ts | 16 + .../common/windowsKeyboardMapper.ts | 86 +- .../browserKeyboardMapper.test.ts | 22 +- .../test/browser/keybindingEditing.test.ts | 315 ++ .../keybindingIO.test.ts | 16 +- .../keybindingEditing.test.ts | 330 -- .../keyboardMapperTestUtils.ts | 16 +- .../macLinuxFallbackKeyboardMapper.test.ts | 115 +- .../macLinuxKeyboardMapper.test.ts | 261 +- .../windowsKeyboardMapper.test.ts | 137 +- .../services/label/common/labelService.ts | 89 +- .../services/label/test/browser/label.test.ts | 60 +- .../label/test/electron-browser/label.test.ts | 10 +- .../services/layout/browser/layoutService.ts | 14 +- .../lifecycle/browser/lifecycleService.ts | 38 +- .../services/lifecycle/common/lifecycle.ts | 10 +- .../lifecycle/common/lifecycleService.ts | 2 +- .../electron-sandbox/lifecycleService.ts | 80 +- .../log/electron-browser/logService.ts | 2 +- .../common/notificationService.ts | 4 +- .../services/outline/browser/outline.ts | 95 + .../outline/browser/outlineService.ts | 52 + .../keybindingsEditorModel.ts | 47 +- .../preferencesEditorInput.ts | 2 +- .../preferences/browser/preferencesService.ts | 4 +- .../preferences/common/preferences.ts | 43 +- .../keybindingsEditorModel.test.ts | 3 +- .../test/browser/progressIndicator.test.ts | 1 + .../remote/browser/remoteAgentServiceImpl.ts | 68 +- .../remote/browser/tunnelServiceImpl.ts | 36 + .../common/abstractRemoteAgentService.ts | 30 +- .../common/remoteAgentEnvironmentChannel.ts | 3 + .../remote/common/remoteExplorerService.ts | 304 +- ...entServiceImpl.ts => tunnelServiceImpl.ts} | 30 +- .../remoteAgentServiceImpl.ts | 71 + .../services/search/common/search.ts | 40 +- .../services/search/common/searchService.ts | 43 +- .../search/electron-browser/searchService.ts | 6 +- .../search/node/ripgrepTextSearchEngine.ts | 24 +- .../electron-browser/rawSearchService.test.ts | 14 +- .../test/node/fileSearch.integrationTest.ts | 4 +- .../test/node/ripgrepFileSearch.test.ts | 12 +- .../test/node/ripgrepTextSearchEngine.test.ts | 27 + .../services/search/test/node/search.test.ts | 95 +- .../test/node/textSearch.integrationTest.ts | 4 +- .../electron-browser/sharedProcessService.ts | 50 - .../electron-browser/telemetryService.ts | 8 +- .../workbenchCommonProperties.ts | 7 +- .../electron-browser/commonProperties.test.ts | 41 +- .../browser/abstractTextMateService.ts | 4 +- .../browser/browserTextFileService.ts | 6 +- .../textfile/browser/textFileService.ts | 30 +- .../textfile/common/textFileEditorModel.ts | 3 + .../common/textFileEditorModelManager.ts | 4 +- .../services/textfile/common/textfiles.ts | 10 +- .../electron-browser/nativeTextFileService.ts | 13 +- .../test/common/textFileService.io.test.ts | 190 +- .../nativeTextFileService.io.test.ts | 36 +- .../test/node/encoding/encoding.test.ts | 72 +- .../themes/browser/workbenchThemeService.ts | 36 +- .../services/themes/common/colorThemeData.ts | 2 +- .../themes/common/iconExtensionPoint.ts | 234 ++ .../themes/common/themeConfiguration.ts | 15 +- .../themes/common/themeExtensionPoints.ts | 7 +- .../themes/common/workbenchThemeService.ts | 8 +- .../test/electron-browser/color-theme.json | 50 + .../tokenStyleResolving.test.ts | 128 +- .../services/timer/browser/timerService.ts | 199 +- .../timer/electron-sandbox/timerService.ts | 3 +- .../test/browser/untitledTextEditor.test.ts | 116 +- .../services/userData/browser/userDataInit.ts | 12 +- .../fileUserDataProvider.test.ts | 52 +- .../userDataAutoSyncEnablementService.ts | 9 +- .../browser/userDataSyncWorkbenchService.ts | 5 +- .../userDataSync/common/userDataSync.ts | 2 +- .../views/browser/viewDescriptorService.ts | 123 +- .../views/common/viewContainerModel.ts | 37 +- .../browser/viewDescriptorService.test.ts | 4 +- .../workingCopyFileOperationParticipant.ts | 59 +- .../common/workingCopyFileService.ts | 137 +- .../browser/workingCopyFileService.test.ts | 133 +- .../test/common/workingCopyService.test.ts | 92 +- .../abstractWorkspaceEditingService.ts | 9 +- .../services/workspaces/browser/workspaces.ts | 23 +- .../workspaces/browser/workspacesService.ts | 5 +- .../workspaceEditingService.ts | 36 +- .../test/browser/workspaces.test.ts | 19 + .../browser/api/extHostApiCommands.test.ts | 386 ++- .../test/browser/api/extHostBulkEdits.test.ts | 6 +- .../test/browser/api/extHostCommands.test.ts | 16 +- .../browser/api/extHostConfiguration.test.ts | 260 +- .../browser/api/extHostDecorations.test.ts | 10 +- .../browser/api/extHostDiagnostics.test.ts | 114 +- .../browser/api/extHostDocumentData.test.ts | 118 +- .../extHostDocumentSaveParticipant.test.ts | 43 +- .../api/extHostDocumentsAndEditors.test.ts | 2 +- .../api/extHostFileSystemEventService.test.ts | 12 +- .../api/extHostLanguageFeatures.test.ts | 236 +- .../api/extHostMessagerService.test.ts | 20 +- .../test/browser/api/extHostNotebook.test.ts | 38 +- .../api/extHostNotebookConcatDocument.test.ts | 84 +- .../test/browser/api/extHostTesting.test.ts | 236 +- .../browser/api/extHostTextEditor.test.ts | 261 +- .../test/browser/api/extHostTreeViews.test.ts | 132 +- .../browser/api/extHostTypeConverter.test.ts | 40 +- .../test/browser/api/extHostTypes.test.ts | 196 +- .../test/browser/api/extHostWorkspace.test.ts | 326 +- .../browser/api/mainThreadCommands.test.ts | 18 +- .../api/mainThreadConfiguration.test.ts | 38 +- .../browser/api/mainThreadDiagnostics.test.ts | 6 +- ...mainThreadDocumentContentProviders.test.ts | 4 +- .../browser/api/mainThreadDocuments.test.ts | 10 +- .../api/mainThreadDocumentsAndEditors.test.ts | 86 +- .../browser/api/mainThreadEditors.test.ts | 54 +- .../browser/api/mainThreadTreeViews.test.ts | 4 +- .../test/browser/api/testRPCProtocol.ts | 5 +- ...Decorations.test.ts => codeeditor.test.ts} | 2 +- src/vs/workbench/test/browser/part.test.ts | 177 +- .../parts/editor/breadcrumbModel.test.ts | 11 +- .../test/browser/parts/editor/editor.test.ts | 114 +- .../parts/editor/editorDiffModel.test.ts | 7 +- .../browser/parts/editor/editorGroups.test.ts | 1638 ++++----- .../browser/parts/editor/editorInput.test.ts | 24 +- .../browser/parts/editor/editorModel.test.ts | 79 +- .../browser/parts/editor/editorPane.test.ts | 51 +- .../parts/editor/resourceEditorInput.test.ts | 6 +- src/vs/workbench/test/browser/viewlet.test.ts | 18 +- .../test/browser/workbenchTestServices.ts | 281 +- src/vs/workbench/test/common/memento.test.ts | 44 +- .../test/common/notifications.test.ts | 140 +- .../test/common/workbenchTestServices.ts | 25 +- .../api/extHostSearch.test.ts | 10 +- .../colorRegistry.releaseTest.ts | 5 +- .../electron-browser/workbenchTestServices.ts | 6 +- .../node/api/extHostTunnelService.test.ts | 266 ++ src/vs/workbench/workbench.common.main.ts | 6 + src/vs/workbench/workbench.desktop.main.ts | 13 +- src/vs/workbench/workbench.web.api.ts | 123 +- src/vs/workbench/workbench.web.main.ts | 3 +- test/.mocharc.json | 5 + test/automation/package.json | 20 +- test/automation/src/application.ts | 6 +- test/automation/src/code.ts | 24 +- test/automation/src/extensions.ts | 6 + test/automation/src/keybindings.ts | 14 +- test/automation/src/playwrightDriver.ts | 3 +- test/automation/yarn.lock | 329 +- test/integration/browser/package.json | 10 +- test/integration/browser/yarn.lock | 18 +- test/integration/electron/testrunner.js | 2 +- test/monaco/.gitignore | 3 + test/monaco/.mocharc.json | 5 + test/monaco/README.md | 13 + test/monaco/core.js | 30 + test/monaco/dist/core.html | 10 + test/monaco/monaco.test.ts | 147 + test/monaco/package.json | 16 + test/monaco/runner.js | 52 + test/monaco/tsconfig.json | 21 + test/monaco/webpack.config.js | 55 + test/monaco/yarn.lock | 52 + test/smoke/package.json | 19 +- .../src/areas/extensions/extensions.test.ts | 17 +- .../smoke/src/areas/notebook/notebook.test.ts | 2 +- .../src/areas/preferences/preferences.test.ts | 2 +- test/smoke/src/areas/search/search.test.ts | 4 +- test/smoke/src/areas/workbench/launch.test.ts | 4 +- .../src/areas/workbench/localization.test.ts | 2 +- test/smoke/src/main.ts | 63 +- test/smoke/src/sql/main.ts | 2 +- test/smoke/src/utils.ts | 6 +- test/smoke/yarn.lock | 699 +--- test/unit/browser/index.js | 21 +- test/unit/browser/renderer.html | 3 +- test/unit/electron/index.js | 37 +- test/unit/electron/renderer.html | 3 +- test/unit/electron/renderer.js | 57 +- test/unit/fullJsonStreamReporter.js | 52 + yarn.lock | 3003 +++++++++++------ 1817 files changed, 81812 insertions(+), 50843 deletions(-) create mode 100644 .github/workflows/build-chat.yml create mode 100644 .vscode/notebooks/papercuts.github-issues delete mode 100644 build/azure-pipelines/common/.gitignore create mode 100644 build/azure-pipelines/common/computeNodeModulesCacheKey.js create mode 100644 build/azure-pipelines/common/computeNodeModulesCacheKey.ts create mode 100644 build/azure-pipelines/common/copyArtifacts.js create mode 100644 build/azure-pipelines/common/createAsset.js create mode 100644 build/azure-pipelines/common/createBuild.js create mode 100644 build/azure-pipelines/common/installPlaywright.js create mode 100644 build/azure-pipelines/common/listNodeModules.js create mode 100644 build/azure-pipelines/common/publish-webview.js create mode 100644 build/azure-pipelines/common/publish.js create mode 100644 build/azure-pipelines/common/release.js create mode 100644 build/azure-pipelines/common/releaseBuild.js create mode 100644 build/azure-pipelines/common/retry.js create mode 100644 build/azure-pipelines/common/retry.ts create mode 100644 build/azure-pipelines/common/sync-mooncake.js delete mode 100644 build/azure-pipelines/darwin/helper-plugin-entitlements.plist delete mode 100644 build/azure-pipelines/publish-types/.gitignore create mode 100644 build/azure-pipelines/publish-types/check-version.js create mode 100644 build/azure-pipelines/publish-types/update-types.js create mode 100644 build/darwin/create-universal-app.js create mode 100644 build/darwin/create-universal-app.ts create mode 100644 build/eslint.js create mode 100644 build/filters.js create mode 100644 build/gulpfile.js create mode 100644 build/lib/builtInExtensions.ts create mode 100644 build/lib/dependencies.js rename build/{dependencies.js => lib/dependencies.ts} (64%) create mode 100644 build/lib/eslint/vscode-dts-cancellation.js create mode 100644 build/lib/eslint/vscode-dts-cancellation.ts create mode 100644 build/lib/eslint/vscode-dts-provider-naming.js create mode 100644 build/lib/eslint/vscode-dts-provider-naming.ts create mode 100644 build/lib/eslint/vscode-dts-region-comments.js create mode 100644 build/lib/eslint/vscode-dts-region-comments.ts create mode 100644 build/lib/eslint/vscode-dts-use-thenable.js create mode 100644 build/lib/eslint/vscode-dts-use-thenable.ts create mode 100644 build/lib/monaco-api.ts rename src/vs/base/browser/ui/codicons/codicon/codicon-animations.css => build/lib/watch/index.ts (64%) create mode 100644 build/lib/watch/watch-win32.ts create mode 100644 build/npm/dirs.js delete mode 100644 build/npm/update-theme.js create mode 100644 extensions/bat/yarn.lock create mode 100644 extensions/docker/yarn.lock create mode 100644 extensions/json/yarn.lock create mode 100644 extensions/markdown-basics/yarn.lock create mode 100644 extensions/markdown-language-features/src/test/urlToUri.test.ts create mode 100644 extensions/markdown-language-features/src/util/url.ts create mode 100644 extensions/microsoft-authentication/src/microsoft-authentication.d.ts create mode 100644 extensions/powershell/yarn.lock create mode 100644 extensions/python/yarn.lock create mode 100644 extensions/r/yarn.lock create mode 100644 extensions/simple-browser/.vscodeignore create mode 100644 extensions/simple-browser/README.md rename extensions/{python => simple-browser}/extension-browser.webpack.config.js (87%) rename extensions/{python => simple-browser}/extension.webpack.config.js (85%) create mode 100644 extensions/simple-browser/media/index.js create mode 100644 extensions/simple-browser/media/index.js.map create mode 100644 extensions/simple-browser/media/main.css create mode 100644 extensions/simple-browser/media/preview-dark.svg create mode 100644 extensions/simple-browser/media/preview-light.svg create mode 100644 extensions/simple-browser/package.json create mode 100644 extensions/simple-browser/package.nls.json create mode 100644 extensions/simple-browser/preview-src/events.ts create mode 100644 extensions/simple-browser/preview-src/index.ts create mode 100644 extensions/simple-browser/preview-src/tsconfig.json create mode 100644 extensions/simple-browser/src/dispose.ts create mode 100644 extensions/simple-browser/src/extension.ts create mode 100644 extensions/simple-browser/src/simpleBrowserManager.ts create mode 100644 extensions/simple-browser/src/simpleBrowserView.ts rename src/vs/editor/contrib/documentSymbols/media/symbol-icons.css => extensions/simple-browser/src/typings/ref.d.ts (74%) create mode 100644 extensions/simple-browser/tsconfig.json rename extensions/{python/src/pythonMain.ts => simple-browser/webpack.config.js} (50%) create mode 100644 extensions/simple-browser/yarn.lock create mode 100644 extensions/sql/yarn.lock create mode 100644 extensions/testing-editor-contributions/.vscodeignore create mode 100644 extensions/testing-editor-contributions/README.md create mode 100644 extensions/testing-editor-contributions/extension-browser.webpack.config.js create mode 100644 extensions/testing-editor-contributions/extension.webpack.config.js create mode 100644 extensions/testing-editor-contributions/package.json create mode 100644 extensions/testing-editor-contributions/package.nls.json create mode 100644 extensions/testing-editor-contributions/src/extension.ts rename src/vs/base/browser/ui/codicons/codicon/codicon-modifications.css => extensions/testing-editor-contributions/src/typings/refs.d.ts (74%) rename extensions/{python => testing-editor-contributions}/tsconfig.json (80%) create mode 100644 extensions/testing-editor-contributions/yarn.lock create mode 100644 extensions/theme-abyss/yarn.lock delete mode 100644 extensions/theme-defaults/themes/hc_black_defaults.json delete mode 100644 extensions/theme-defaults/themes/light_defaults.json create mode 100644 extensions/theme-defaults/yarn.lock create mode 100644 extensions/theme-kimbie-dark/yarn.lock create mode 100644 extensions/theme-monokai-dimmed/yarn.lock create mode 100644 extensions/theme-monokai/yarn.lock create mode 100644 extensions/theme-quietlight/yarn.lock create mode 100644 extensions/theme-red/yarn.lock create mode 100644 extensions/theme-seti/icons/preview.html create mode 100644 extensions/theme-seti/yarn.lock create mode 100644 extensions/theme-solarized-dark/yarn.lock create mode 100644 extensions/theme-solarized-light/yarn.lock create mode 100644 extensions/theme-tomorrow-night-blue/yarn.lock create mode 100644 extensions/typescript-basics/build/update-grammars.js create mode 100644 extensions/typescript-basics/yarn.lock rename extensions/{docker => vscode-colorize-tests}/test/colorize-fixtures/Dockerfile (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-fixtures/issue-1550.yaml (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-fixtures/issue-4008.yaml (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-fixtures/issue-6303.yaml (100%) rename extensions/{markdown-basics => vscode-colorize-tests}/test/colorize-fixtures/test-33886.md (100%) rename extensions/{xml => vscode-colorize-tests}/test/colorize-fixtures/test-7115.xml (100%) rename extensions/{powershell => vscode-colorize-tests}/test/colorize-fixtures/test-freeze-56476.ps1 (100%) rename extensions/{bat => vscode-colorize-tests}/test/colorize-fixtures/test.bat (100%) rename extensions/{json => vscode-colorize-tests}/test/colorize-fixtures/test.json (100%) rename extensions/{markdown-basics => vscode-colorize-tests}/test/colorize-fixtures/test.md (100%) rename extensions/{powershell => vscode-colorize-tests}/test/colorize-fixtures/test.ps1 (100%) rename extensions/{r => vscode-colorize-tests}/test/colorize-fixtures/test.r (100%) rename extensions/{sql => vscode-colorize-tests}/test/colorize-fixtures/test.sql (100%) rename extensions/{xml => vscode-colorize-tests}/test/colorize-fixtures/test.xml (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-fixtures/test.yaml (100%) rename extensions/{docker => vscode-colorize-tests}/test/colorize-results/Dockerfile.json (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-results/issue-1550_yaml.json (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-results/issue-4008_yaml.json (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-results/issue-6303_yaml.json (100%) rename extensions/{markdown-basics => vscode-colorize-tests}/test/colorize-results/test-33886_md.json (100%) rename extensions/{xml => vscode-colorize-tests}/test/colorize-results/test-7115_xml.json (100%) rename extensions/{powershell => vscode-colorize-tests}/test/colorize-results/test-freeze-56476_ps1.json (100%) rename extensions/{bat => vscode-colorize-tests}/test/colorize-results/test_bat.json (100%) rename extensions/{json => vscode-colorize-tests}/test/colorize-results/test_json.json (100%) rename extensions/{markdown-basics => vscode-colorize-tests}/test/colorize-results/test_md.json (100%) rename extensions/{powershell => vscode-colorize-tests}/test/colorize-results/test_ps1.json (100%) rename extensions/{r => vscode-colorize-tests}/test/colorize-results/test_r.json (100%) rename extensions/{sql => vscode-colorize-tests}/test/colorize-results/test_sql.json (100%) rename extensions/{xml => vscode-colorize-tests}/test/colorize-results/test_xml.json (100%) rename extensions/{yaml => vscode-colorize-tests}/test/colorize-results/test_yaml.json (100%) create mode 100644 extensions/xml/yarn.lock create mode 100644 extensions/yaml/yarn.lock create mode 100644 src/sql/workbench/browser/actions/layoutActions.ts rename src/sql/{platform => workbench/contrib}/opener/common/openerServiceStub.ts (75%) create mode 100644 src/tsconfig.tsec.json create mode 100644 src/tsec.exemptions.json create mode 100644 src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css rename src/vs/base/browser/{codicons.ts => ui/iconLabel/iconLabels.ts} (56%) rename src/vs/base/browser/ui/{codicons/codiconLabel.ts => iconLabel/simpleIconLabel.ts} (76%) delete mode 100644 src/vs/base/common/buildunit.json create mode 100644 src/vs/base/common/iconLabels.ts create mode 100644 src/vs/base/node/powershell.ts create mode 100644 src/vs/base/node/shell.ts create mode 100644 src/vs/base/parts/ipc/browser/ipc.mp.ts create mode 100644 src/vs/base/parts/ipc/common/ipc.mp.ts rename src/vs/base/parts/ipc/electron-main/{ipc.electron-main.ts => ipc.electron.ts} (90%) create mode 100644 src/vs/base/parts/ipc/electron-main/ipc.mp.ts rename src/vs/base/parts/ipc/electron-sandbox/{ipc.electron-sandbox.ts => ipc.electron.ts} (71%) create mode 100644 src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts create mode 100644 src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts create mode 100644 src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts delete mode 100644 src/vs/base/test/browser/codicons.test.ts create mode 100644 src/vs/base/test/browser/iconLabels.test.ts rename src/vs/base/test/{node => common}/buffer.test.ts (70%) rename src/vs/base/test/{node => common}/console.test.ts (99%) rename src/vs/base/test/{node => common}/glob.test.ts (98%) create mode 100644 src/vs/base/test/common/iconLabels.test.ts rename src/vs/base/test/{node => common}/path.test.ts (99%) create mode 100644 src/vs/base/test/common/troubleshooting.ts create mode 100644 src/vs/base/test/node/powershell.test.ts delete mode 100644 src/vs/buildunit.json create mode 100644 src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts create mode 100644 src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts delete mode 100644 src/vs/code/electron-main/auth2.ts delete mode 100644 src/vs/code/electron-sandbox/proxy/auth.html delete mode 100644 src/vs/code/electron-sandbox/proxy/auth.js create mode 100644 src/vs/editor/common/services/getSemanticTokens.ts rename src/vs/{workbench/api/common/shared => editor/common/services}/semanticTokensDto.ts (100%) rename src/vs/editor/contrib/{gotoSymbol => documentSymbols}/documentSymbols.ts (54%) delete mode 100644 src/vs/editor/contrib/documentSymbols/outline.ts create mode 100644 src/vs/editor/contrib/hover/markdownHoverParticipant.ts create mode 100644 src/vs/editor/contrib/hover/markerHoverParticipant.ts create mode 100644 src/vs/editor/contrib/inlineHints/inlineHintsController.ts rename src/vs/editor/contrib/{documentSymbols/outlineTree.ts => symbolIcons/symbolIcons.ts} (57%) rename src/vs/{workbench/test/common/api => editor/test/common/services}/semanticTokensDto.test.ts (95%) create mode 100644 src/vs/editor/test/common/services/testTextResourcePropertiesService.ts create mode 100644 src/vs/platform/actions/browser/menuEntryActionViewItem.css rename src/vs/platform/dialogs/electron-main/{dialogs.ts => dialogMainService.ts} (54%) create mode 100644 src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts create mode 100644 src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts delete mode 100644 src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts delete mode 100644 src/vs/platform/ipc/electron-main/sharedProcessMainService.ts create mode 100644 src/vs/platform/sharedProcess/node/sharedProcess.ts rename src/vs/platform/telemetry/{node => common}/commonProperties.ts (80%) rename src/vs/platform/telemetry/{node => common}/telemetryIpc.ts (100%) create mode 100644 src/vs/platform/theme/browser/iconsStyleSheet.ts create mode 100644 src/vs/platform/windows/electron-main/windowsFinder.ts create mode 100644 src/vs/platform/windows/electron-main/windowsStateHandler.ts delete mode 100644 src/vs/platform/windows/electron-main/windowsStateStorage.ts delete mode 100644 src/vs/platform/windows/node/window.ts rename src/vs/platform/windows/{common => node}/windowTracker.ts (100%) create mode 100644 src/vs/platform/windows/test/electron-main/window.test.ts delete mode 100644 src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts delete mode 100644 src/vs/platform/windows/test/node/window.test.ts create mode 100644 src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts delete mode 100644 src/vs/platform/workspaces/electron-main/workspacesService.ts create mode 100644 src/vs/platform/workspaces/test/common/workspaces.test.ts rename src/vs/platform/workspaces/test/electron-main/{workspacesMainService.test.ts => workspacesManagementMainService.test.ts} (66%) create mode 100644 src/vs/workbench/api/browser/mainThreadCLICommands.ts create mode 100644 src/vs/workbench/api/browser/mainThreadEditorTabs.ts create mode 100644 src/vs/workbench/api/browser/mainThreadSecretState.ts create mode 100644 src/vs/workbench/api/browser/mainThreadUriOpeners.ts create mode 100644 src/vs/workbench/api/common/exHostSecretState.ts create mode 100644 src/vs/workbench/api/common/extHostEditorTabs.ts create mode 100644 src/vs/workbench/api/common/extHostSecrets.ts create mode 100644 src/vs/workbench/api/common/extHostUriOpener.ts rename src/vs/workbench/browser/{parts/editor/editorWidgets.ts => codeeditor.ts} (60%) create mode 100644 src/vs/workbench/browser/menuActions.ts delete mode 100644 src/vs/workbench/browser/parts/views/viewMenuActions.ts create mode 100644 src/vs/workbench/browser/parts/views/viewPane.ts create mode 100644 src/vs/workbench/browser/window.ts rename src/vs/workbench/contrib/backup/test/{electron-browser => browser}/backupRestorer.test.ts (52%) create mode 100644 src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts create mode 100644 src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts rename src/vs/{editor/contrib/documentSymbols/media/outlineTree.css => workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css} (77%) create mode 100644 src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts delete mode 100644 src/vs/workbench/contrib/debug/browser/debugActions.ts rename src/vs/workbench/contrib/debug/test/{electron-browser => browser}/debugANSIHandling.test.ts (100%) delete mode 100644 src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts create mode 100644 src/vs/workbench/contrib/externalUriOpener/common/configuration.ts create mode 100644 src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts create mode 100644 src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts create mode 100644 src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts create mode 100644 src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts delete mode 100644 src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css create mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/diff/common.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel.ts create mode 100644 src/vs/workbench/contrib/outline/browser/outlineViewState.ts create mode 100644 src/vs/workbench/contrib/remote/browser/remoteIcons.ts create mode 100644 src/vs/workbench/contrib/tags/common/javaWorkspaceTags.ts rename src/vs/workbench/contrib/terminal/test/{node => common}/terminalEnvironment.test.ts (97%) create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/locationStore.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/stateByName.ts create mode 100644 src/vs/workbench/contrib/testing/browser/explorerProjections/stateNodes.ts create mode 100644 src/vs/workbench/contrib/testing/browser/icons.ts create mode 100644 src/vs/workbench/contrib/testing/browser/media/testing.css create mode 100644 src/vs/workbench/contrib/testing/browser/testExplorerActions.ts create mode 100644 src/vs/workbench/contrib/testing/browser/testingDecorations.ts create mode 100644 src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts create mode 100644 src/vs/workbench/contrib/testing/browser/testingExplorerView.ts create mode 100644 src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts create mode 100644 src/vs/workbench/contrib/testing/browser/testingViewPaneContainer.ts create mode 100644 src/vs/workbench/contrib/testing/browser/theme.ts create mode 100644 src/vs/workbench/contrib/testing/common/constants.ts create mode 100644 src/vs/workbench/contrib/testing/common/ownedTestCollection.ts create mode 100644 src/vs/workbench/contrib/testing/common/storedValue.ts create mode 100644 src/vs/workbench/contrib/testing/common/testResultService.ts create mode 100644 src/vs/workbench/contrib/testing/common/testStubs.ts create mode 100644 src/vs/workbench/contrib/testing/common/testingContentProvider.ts create mode 100644 src/vs/workbench/contrib/testing/common/testingContextKeys.ts create mode 100644 src/vs/workbench/contrib/testing/common/testingStates.ts create mode 100644 src/vs/workbench/contrib/testing/common/testingUri.ts create mode 100644 src/vs/workbench/contrib/testing/common/workspaceTestCollectionService.ts create mode 100644 src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts create mode 100644 src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts create mode 100644 src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByLocation.test.ts create mode 100644 src/vs/workbench/contrib/testing/test/browser/explorerProjections/stateByName.test.ts create mode 100644 src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts create mode 100644 src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts create mode 100644 src/vs/workbench/contrib/testing/test/common/testingUri.test.ts create mode 100644 src/vs/workbench/contrib/url/common/urlGlob.ts create mode 100644 src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedIcons.ts delete mode 100644 src/vs/workbench/electron-browser/actions/developerActions.ts create mode 100644 src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts rename src/vs/workbench/services/configuration/{electron-browser => electron-sandbox}/configurationCache.ts (54%) create mode 100644 src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts create mode 100644 src/vs/workbench/services/configuration/test/browser/configurationService.test.ts delete mode 100644 src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts delete mode 100644 src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts create mode 100644 src/vs/workbench/services/extensions/common/polyfillNestedWorker.protocol.ts create mode 100644 src/vs/workbench/services/extensions/test/browser/extensionService.test.ts create mode 100644 src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts delete mode 100644 src/vs/workbench/services/gettingStarted/common/media/ColorTheme.jpg delete mode 100644 src/vs/workbench/services/gettingStarted/common/media/Extensions.jpg delete mode 100644 src/vs/workbench/services/gettingStarted/common/media/Languages.jpg delete mode 100644 src/vs/workbench/services/gettingStarted/common/media/OpenFolder.jpg create mode 100644 src/vs/workbench/services/gettingStarted/common/media/colorTheme.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/commandPalette.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/dark/settings.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/extensions.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/forwardPorts.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/github.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/hc/settings.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/languageExtensions.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/light/settings.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/openFolder.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/openVSC.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/pullRequests.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/remoteTerminal.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/runProject.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/settingsSync.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/terminal.png create mode 100644 src/vs/workbench/services/gettingStarted/common/media/tutorialVideo.png create mode 100644 src/vs/workbench/services/keybinding/browser/unboundCommands.ts rename src/vs/workbench/services/keybinding/test/{electron-browser => browser}/browserKeyboardMapper.test.ts (88%) create mode 100644 src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts rename src/vs/workbench/services/keybinding/test/{electron-browser => browser}/keybindingIO.test.ts (95%) delete mode 100644 src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts create mode 100644 src/vs/workbench/services/outline/browser/outline.ts create mode 100644 src/vs/workbench/services/outline/browser/outlineService.ts rename src/vs/workbench/services/preferences/{common => browser}/keybindingsEditorModel.ts (94%) rename src/vs/workbench/services/preferences/{common => browser}/preferencesEditorInput.ts (99%) rename src/vs/workbench/services/preferences/test/{common => browser}/keybindingsEditorModel.test.ts (99%) create mode 100644 src/vs/workbench/services/remote/browser/tunnelServiceImpl.ts rename src/vs/workbench/services/remote/electron-browser/{remoteAgentServiceImpl.ts => tunnelServiceImpl.ts} (60%) create mode 100644 src/vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl.ts delete mode 100644 src/vs/workbench/services/sharedProcess/electron-browser/sharedProcessService.ts rename src/vs/workbench/services/telemetry/{electron-browser => electron-sandbox}/workbenchCommonProperties.ts (88%) create mode 100644 src/vs/workbench/services/themes/common/iconExtensionPoint.ts create mode 100644 src/vs/workbench/services/themes/test/electron-browser/color-theme.json rename src/vs/workbench/services/userData/test/{electron-browser => browser}/fileUserDataProvider.test.ts (90%) create mode 100644 src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts rename src/vs/workbench/test/browser/{parts/editor/rangeDecorations.test.ts => codeeditor.test.ts} (99%) create mode 100644 src/vs/workbench/test/node/api/extHostTunnelService.test.ts create mode 100644 test/.mocharc.json create mode 100644 test/monaco/.gitignore create mode 100644 test/monaco/.mocharc.json create mode 100644 test/monaco/README.md create mode 100644 test/monaco/core.js create mode 100644 test/monaco/dist/core.html create mode 100644 test/monaco/monaco.test.ts create mode 100644 test/monaco/package.json create mode 100644 test/monaco/runner.js create mode 100644 test/monaco/tsconfig.json create mode 100644 test/monaco/webpack.config.js create mode 100644 test/monaco/yarn.lock create mode 100644 test/unit/fullJsonStreamReporter.js diff --git a/.eslintignore b/.eslintignore index b2c4a5b6ef..5a8dd89ec5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,3 +14,4 @@ **/extensions/**/build/** **/extensions/markdown-language-features/media/** **/extensions/typescript-basics/test/colorize-fixtures/** +**/extensions/**/dist/** diff --git a/.eslintrc.json b/.eslintrc.json index cf754b6584..fc9c8f771c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -939,6 +939,13 @@ "*" // node modules ] }, + { + "target": "**/test/monaco/**", + "restrictions": [ + "**/test/monaco/**", + "*" // node modules + ] + }, { "target": "**/api/**.test.ts", "restrictions": [ @@ -946,9 +953,9 @@ "assert", "sinon", "crypto", - "vscode", - "typemoq", - "azdata" + "vscode", + "typemoq", + "azdata" ] }, { diff --git a/.github/workflows/build-chat.yml b/.github/workflows/build-chat.yml new file mode 100644 index 0000000000..f9f146164b --- /dev/null +++ b/.github/workflows/build-chat.yml @@ -0,0 +1,35 @@ +name: "Build Chat" + +on: + workflow_run: + workflows: + - CI + types: + - completed + branches: + - master + - release/* + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: "microsoft/vscode-github-triage-actions" + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Install Additional Dependencies + # Pulls in a bunch of other packages that arent needed for the rest of the actions + run: npm install @azure/storage-blob@12.1.1 + - name: Build Chat + uses: ./actions/build-chat + with: + token: ${{ secrets.GITHUB_TOKEN }} + slack_token: ${{ secrets.SLACK_TOKEN }} + storage_connection_string: ${{ secrets.BUILD_CHAT_STORAGE_CONNECTION_STRING }} + workflow_run_url: ${{ github.event.workflow_run.url }} + notification_channel: build + log_channel: bot-log diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index ffa11badc9..af42751b6f 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v42 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index 1ce6faed4b..837e569689 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v42 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index 190da52ba3..bc4b6b5579 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -14,7 +14,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v42 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.gitignore b/.gitignore index f44cadebe5..95104a2520 100644 --- a/.gitignore +++ b/.gitignore @@ -5,26 +5,8 @@ Thumbs.db node_modules/ .build/ extensions/**/dist/ -out/ -out-build/ -out-editor/ -out-editor-src/ -out-editor-build/ -out-editor-esm/ -out-editor-esm-bundle/ -out-editor-min/ -out-monaco-editor-core/ -out-vscode/ -out-vscode-min/ -out-vscode-reh/ -out-vscode-reh-min/ -out-vscode-reh-pkg/ -out-vscode-reh-web/ -out-vscode-reh-web-min/ -out-vscode-reh-web-pkg/ -out-vscode-web/ -out-vscode-web-min/ -out-vscode-web-pkg/ +/out*/ +/extensions/**/out/ src/vs/server resources/server build/node_modules diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2cd0a32b9c..e8d77d395a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,6 @@ // for the documentation about the extensions.json format "recommendations": [ "dbaeumer.vscode-eslint", - "EditorConfig.EditorConfig", - "msjsdiag.debugger-for-chrome" + "EditorConfig.EditorConfig" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 6fc69d627b..95319f98bb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -105,8 +105,7 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], - "browserLaunchLocation": "workspace", - "preLaunchTask": "Ensure Prelaunch Dependencies", + "browserLaunchLocation": "workspace" }, { "type": "node", diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 8ff55e2c6e..71fd646776 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -8,7 +8,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"November 2020\"", + "value": "$repo=repo:microsoft/vscode\n$milestone=milestone:\"February 2021\"", "editable": true }, { diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 35a22f829e..5948e648c3 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -8,8 +8,8 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server\n\n$MILESTONE=milestone:\"November 2020\"", - "editable": false + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper\n\n$MILESTONE=milestone:\"January 2021\"", + "editable": true }, { "kind": 1, @@ -80,7 +80,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:closed label:feature-request label:verification-needed", + "value": "$REPOS $MILESTONE is:issue is:closed label:feature-request label:verification-needed -label:verified", "editable": true }, { @@ -89,10 +89,28 @@ "value": "# Verification", "editable": true }, + { + "kind": 1, + "language": "markdown", + "value": "## Verifiable Fixes", + "editable": true + }, { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:closed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found", + "value": "$REPOS $MILESTONE is:issue is:closed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -label:z-author-verified -label:unreleased", + "editable": true + }, + { + "kind": 1, + "language": "markdown", + "value": "## Unreleased Fixes", + "editable": true + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS $MILESTONE is:issue is:closed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -label:z-author-verified label:unreleased", "editable": true }, { @@ -104,7 +122,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE label:candidate", + "value": "$REPOS $MILESTONE is:open label:candidate", "editable": true } ] \ No newline at end of file diff --git a/.vscode/notebooks/grooming-delta.github-issues b/.vscode/notebooks/grooming-delta.github-issues index 8bc9d38b48..6602ea4b1c 100644 --- a/.vscode/notebooks/grooming-delta.github-issues +++ b/.vscode/notebooks/grooming-delta.github-issues @@ -176,17 +176,20 @@ { "kind": 1, "language": "markdown", - "value": "# vscode-pull-request-github" + "value": "# vscode-pull-request-github", + "editable": true }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-pull-request-github is:issue closed:>$since" + "value": "repo:microsoft/vscode-pull-request-github is:issue closed:>$since", + "editable": true }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-test is:issue created:>$since" + "value": "repo:microsoft/vscode-test is:issue created:>$since", + "editable": true }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 402ef474e5..0f14e5df08 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -8,8 +8,8 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server\n\n$MILESTONE=milestone:\"November 2020\"\n\n$MINE=assignee:@me", - "editable": false + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server\n\n$MILESTONE=milestone:\"January 2021\"\n\n$MINE=assignee:@me", + "editable": true }, { "kind": 1, @@ -122,7 +122,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE $MINE is:issue is:open", + "value": "$REPOS $MILESTONE $MINE is:issue is:open -label:endgame-plan", "editable": true }, { @@ -152,7 +152,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE $MINE is:issue is:open label:bug label:verification-steps-needed", + "value": "$REPOS $MILESTONE $MINE is:issue label:bug label:verification-steps-needed", "editable": true }, { @@ -164,7 +164,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE $MINE is:issue is:open label:bug label:verification-found", + "value": "$REPOS $MILESTONE $MINE is:issue label:bug label:verification-found", "editable": true }, { @@ -176,7 +176,19 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed author:@me sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found", + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed author:@me sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found", + "editable": true + }, + { + "kind": 1, + "language": "markdown", + "value": "## Issues filed from outside team", + "editable": true + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:btholt -author:chrisdias -author:chrmarti -author:Chuxel -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:eamodio -author:egamma -author:fiveisprime -author:gregvanl -author:isidorn -author:ItalyPaleAle -author:JacksonKearl -author:joaomoreno -author:jrieken -author:kieferrm -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:ornellaalt -author:orta -author:rebornix -author:RMacfarlane -author:roblourens -author:rzhao271 -author:sana-ajani -author:sandy081 -author:sbatten -author:stevencl -author:Tyriar -author:weinand -author:TylerLeonhardt -author:lramos15", "editable": true }, { @@ -188,7 +200,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed -author:@me sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found", + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed -author:@me sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found", "editable": true }, { diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index 55fc585e3e..a566e7309f 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -8,7 +8,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"November 2020\"", + "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog\n\n// current milestone name\n$milestone=milestone:\"February 2021\"", "editable": true }, { @@ -83,6 +83,24 @@ "value": "$repos assignee:@me is:open milestone:\"Backlog Candidates\"", "editable": false }, + { + "kind": 1, + "language": "markdown", + "value": "### Personal Inbox\n", + "editable": true + }, + { + "kind": 1, + "language": "markdown", + "value": "\n#### Missing Type label", + "editable": true + }, + { + "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", + "editable": true + }, { "kind": 1, "language": "markdown", diff --git a/.vscode/notebooks/papercuts.github-issues b/.vscode/notebooks/papercuts.github-issues new file mode 100644 index 0000000000..4da4734ce5 --- /dev/null +++ b/.vscode/notebooks/papercuts.github-issues @@ -0,0 +1,44 @@ +[ + { + "kind": 1, + "language": "markdown", + "value": "## Papercuts\n\nThis notebook serves as an ongoing collection of papercut issues that we encounter while dogfooding. With that in mind only promote issues that really turn you off, e.g. issues that make you want to stop using VS Code or its extensions. To mark an issue (bug, feature-request, etc.) as papercut add the labels: `papercut :drop_of_blood:`", + "editable": true + }, + { + "kind": 1, + "language": "markdown", + "value": "## All Papercuts\n\nThese are all papercut issues that we encounter while dogfooding vscode or extensions that we author.", + "editable": true + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode is:open -label:notebook label:\"papercut :drop_of_blood:\"", + "editable": true + }, + { + "kind": 1, + "language": "markdown", + "value": "## Native Notebook", + "editable": true + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode is:open label:notebook label:\"papercut :drop_of_blood:\"", + "editable": true + }, + { + "kind": 1, + "language": "markdown", + "value": "### My Papercuts", + "editable": true + }, + { + "kind": 2, + "language": "github-issues", + "value": "repo:microsoft/vscode is:open assignee:@me label:\"papercut :drop_of_blood:\"", + "editable": true + } +] diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index 67db0cb97d..e223523d44 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -14,7 +14,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks \n$milestone=milestone:\"October 2020\"", + "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks \n$milestone=milestone:\"January 2021\"", "editable": true }, { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1d83e2301d..e9d338d885 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -62,7 +62,8 @@ "group": { "kind": "build", "isDefault": true - } + }, + "problemMatcher": [] }, { "type": "npm", @@ -90,7 +91,8 @@ "Kill Build VS Code Core", "Kill Build VS Code Extensions" ], - "group": "build" + "group": "build", + "problemMatcher": [] }, { "type": "npm", @@ -217,7 +219,7 @@ "base": "$tsc", "applyTo": "allDocuments", "owner": "tsec" - }, + } ], "group": "build", "label": "npm: tsec-compile-check", diff --git a/.yarnrc b/.yarnrc index c469f9a767..9738cd535a 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://electronjs.org/headers" -target "9.4.3" +target "11.2.2" runtime "electron" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4a20ee4cd0..dbf204bcd1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,3 +20,8 @@ jobs: vmImage: macOS-latest steps: - template: build/azure-pipelines/darwin/continuous-build-darwin.yml + +trigger: + branches: + exclude: + - electron-11.x.y diff --git a/build/.moduleignore b/build/.moduleignore index bd49233e3d..4d76472b36 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -1,4 +1,6 @@ -# cleanup rules for native node modules, .gitignore style +# cleanup rules for node modules, .gitignore style + +# native node modules nan/** */node_modules/nan/** @@ -46,6 +48,7 @@ spdlog/build/** spdlog/deps/** spdlog/src/** spdlog/test/** +spdlog/*.yml !spdlog/build/Release/*.node jschardet/dist/** @@ -72,6 +75,7 @@ node-pty/build/** node-pty/src/** node-pty/tools/** node-pty/deps/** +node-pty/scripts/** !node-pty/build/Release/*.exe !node-pty/build/Release/*.dll !node-pty/build/Release/*.node @@ -117,8 +121,55 @@ vsda/README.md vsda/targets !vsda/build/Release/vsda.node +vscode-encrypt/build/** +vscode-encrypt/src/** +vscode-encrypt/vendor/** +vscode-encrypt/.gitignore +vscode-encrypt/binding.gyp +vscode-encrypt/README.md +!vscode-encrypt/build/Release/vscode-encrypt-native.node + vscode-windows-ca-certs/**/* !vscode-windows-ca-certs/package.json !vscode-windows-ca-certs/**/*.node node-addon-api/**/* + +# other node modules + +**/docs/** +**/example/** +**/examples/** +**/test/** +**/tests/** + +**/History.md +**/CHANGELOG.md +**/README.md +**/readme.md +**/readme.markdown + +**/*.ts +!typescript/**/*.d.ts + +jschardet/dist/** + +es6-promise/lib/** + +vscode-textmate/webpack.config.js + +# {{SQL CARBON EDIT }} We need more than just zone-node.js +# zone.js/dist/** +# !zone.js/dist/zone-node.js + +# https://github.com/xtermjs/xterm.js/issues/3137 +xterm/src/** +xterm/tsconfig.all.json + +# https://github.com/xtermjs/xterm.js/issues/3138 +xterm-addon-*/src/** +xterm-addon-*/fixtures/** +xterm-addon-*/out/** +xterm-addon-*/out-test/** + + diff --git a/build/azure-pipelines/common/.gitignore b/build/azure-pipelines/common/.gitignore deleted file mode 100644 index e94ecda764..0000000000 --- a/build/azure-pipelines/common/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -*.js \ No newline at end of file diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.js b/build/azure-pipelines/common/computeNodeModulesCacheKey.js new file mode 100644 index 0000000000..e607dee136 --- /dev/null +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.js @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { dirs } = require('../../npm/dirs'); +const ROOT = path.join(__dirname, '../../../'); +const shasum = crypto.createHash('sha1'); +shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt'))); +shasum.update(fs.readFileSync(path.join(ROOT, '.yarnrc'))); +shasum.update(fs.readFileSync(path.join(ROOT, 'remote/.yarnrc'))); +// Add `yarn.lock` files +for (let dir of dirs) { + const yarnLockPath = path.join(ROOT, dir, 'yarn.lock'); + shasum.update(fs.readFileSync(yarnLockPath)); +} +// Add any other command line arguments +for (let i = 2; i < process.argv.length; i++) { + shasum.update(process.argv[i]); +} +process.stdout.write(shasum.digest('hex')); diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts new file mode 100644 index 0000000000..9bcd769591 --- /dev/null +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +const { dirs } = require('../../npm/dirs'); + +const ROOT = path.join(__dirname, '../../../'); + +const shasum = crypto.createHash('sha1'); + +shasum.update(fs.readFileSync(path.join(ROOT, 'build/.cachesalt'))); +shasum.update(fs.readFileSync(path.join(ROOT, '.yarnrc'))); +shasum.update(fs.readFileSync(path.join(ROOT, 'remote/.yarnrc'))); + +// Add `yarn.lock` files +for (let dir of dirs) { + const yarnLockPath = path.join(ROOT, dir, 'yarn.lock'); + shasum.update(fs.readFileSync(yarnLockPath)); +} + +// Add any other command line arguments +for (let i = 2; i < process.argv.length; i++) { + shasum.update(process.argv[i]); +} + +process.stdout.write(shasum.digest('hex')); diff --git a/build/azure-pipelines/common/copyArtifacts.js b/build/azure-pipelines/common/copyArtifacts.js new file mode 100644 index 0000000000..45f5e4f481 --- /dev/null +++ b/build/azure-pipelines/common/copyArtifacts.js @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const vfs = require("vinyl-fs"); +const path = require("path"); +const es = require("event-stream"); +const fs = require("fs"); +const files = [ + '.build/extensions/**/*.vsix', + '.build/win32-x64/**/*.{exe,zip}', + '.build/linux/sha256hashes.txt', + '.build/linux/deb/amd64/deb/*.deb', + '.build/linux/rpm/x86_64/*.rpm', + '.build/linux/server/*', + '.build/linux/archive/*', + '.build/docker/*', + '.build/darwin/*', + '.build/version.json' // version information +]; +async function main() { + return new Promise((resolve, reject) => { + const stream = vfs.src(files, { base: '.build', allowEmpty: true }) + .pipe(es.through(file => { + const filePath = path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, + //Preserve intermediate directories after .build folder + file.path.substr(path.resolve('.build').length + 1)); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.renameSync(file.path, filePath); + })); + stream.on('end', () => resolve()); + stream.on('error', e => reject(e)); + }); +} +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/common/createAsset.js b/build/azure-pipelines/common/createAsset.js new file mode 100644 index 0000000000..40e674c271 --- /dev/null +++ b/build/azure-pipelines/common/createAsset.js @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const crypto = require("crypto"); +const azure = require("azure-storage"); +const mime = require("mime"); +const cosmos_1 = require("@azure/cosmos"); +const retry_1 = require("./retry"); +if (process.argv.length !== 6) { + console.error('Usage: node createAsset.js PLATFORM TYPE NAME FILE'); + process.exit(-1); +} +function hashStream(hashName, stream) { + return new Promise((c, e) => { + const shasum = crypto.createHash(hashName); + stream + .on('data', shasum.update.bind(shasum)) + .on('error', e) + .on('close', () => c(shasum.digest('hex'))); + }); +} +async function doesAssetExist(blobService, quality, blobName) { + const existsResult = await new Promise((c, e) => blobService.doesBlobExist(quality, blobName, (err, r) => err ? e(err) : c(r))); + return existsResult.exists; +} +async function uploadBlob(blobService, quality, blobName, filePath, fileName) { + const blobOptions = { + contentSettings: { + contentType: mime.lookup(filePath), + contentDisposition: `attachment; filename="${fileName}"`, + cacheControl: 'max-age=31536000, public' + } + }; + await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, filePath, blobOptions, err => err ? e(err) : c())); +} +function getEnv(name) { + const result = process.env[name]; + if (typeof result === 'undefined') { + throw new Error('Missing env: ' + name); + } + return result; +} +async function main() { + const [, , platform, type, fileName, filePath] = process.argv; + const quality = getEnv('VSCODE_QUALITY'); + const commit = getEnv('BUILD_SOURCEVERSION'); + console.log('Creating asset...'); + const stat = await new Promise((c, e) => fs.stat(filePath, (err, stat) => err ? e(err) : c(stat))); + const size = stat.size; + console.log('Size:', size); + const stream = fs.createReadStream(filePath); + const [sha1hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); + console.log('SHA1:', sha1hash); + console.log('SHA256:', sha256hash); + const blobName = commit + '/' + fileName; + const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']; + const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + const blobExists = await doesAssetExist(blobService, quality, blobName); + if (blobExists) { + console.log(`Blob ${quality}, ${blobName} already exists, not publishing again.`); + return; + } + console.log('Uploading blobs to Azure storage...'); + await uploadBlob(blobService, quality, blobName, filePath, fileName); + console.log('Blobs successfully uploaded.'); + const asset = { + platform, + type, + url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, + hash: sha1hash, + sha256hash, + size + }; + // Remove this if we ever need to rollback fast updates for windows + if (/win32/.test(platform)) { + asset.supportsFastUpdate = true; + } + console.log('Asset:', JSON.stringify(asset, null, ' ')); + const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const scripts = client.database('builds').container(quality).scripts; + await retry_1.retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); +} +main().then(() => { + console.log('Asset successfully created'); + process.exit(0); +}, err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index f06c6bbcbf..4fee172297 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -11,6 +11,7 @@ import * as crypto from 'crypto'; import * as azure from 'azure-storage'; import * as mime from 'mime'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; interface Asset { platform: string; @@ -121,7 +122,7 @@ async function main(): Promise { const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]); + await retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); } main().then(() => { diff --git a/build/azure-pipelines/common/createBuild.js b/build/azure-pipelines/common/createBuild.js new file mode 100644 index 0000000000..2165a62b8c --- /dev/null +++ b/build/azure-pipelines/common/createBuild.js @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const cosmos_1 = require("@azure/cosmos"); +const retry_1 = require("./retry"); +if (process.argv.length !== 3) { + console.error('Usage: node createBuild.js VERSION'); + process.exit(-1); +} +function getEnv(name) { + const result = process.env[name]; + if (typeof result === 'undefined') { + throw new Error('Missing env: ' + name); + } + return result; +} +async function main() { + const [, , _version] = process.argv; + const quality = getEnv('VSCODE_QUALITY'); + const commit = getEnv('BUILD_SOURCEVERSION'); + const queuedBy = getEnv('BUILD_QUEUEDBY'); + const sourceBranch = getEnv('BUILD_SOURCEBRANCH'); + const version = _version + (quality === 'stable' ? '' : `-${quality}`); + console.log('Creating build...'); + console.log('Quality:', quality); + console.log('Version:', version); + console.log('Commit:', commit); + const build = { + id: commit, + timestamp: (new Date()).getTime(), + version, + isReleased: false, + sourceBranch, + queuedBy, + assets: [], + updates: {} + }; + const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const scripts = client.database('builds').container(quality).scripts; + await retry_1.retry(() => scripts.storedProcedure('createBuild').execute('', [Object.assign(Object.assign({}, build), { _partitionKey: '' })])); +} +main().then(() => { + console.log('Build successfully created'); + process.exit(0); +}, err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/common/createBuild.ts b/build/azure-pipelines/common/createBuild.ts index eecf0e945e..632971ea2c 100644 --- a/build/azure-pipelines/common/createBuild.ts +++ b/build/azure-pipelines/common/createBuild.ts @@ -6,6 +6,7 @@ 'use strict'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; if (process.argv.length !== 3) { console.error('Usage: node createBuild.js VERSION'); @@ -48,7 +49,7 @@ async function main(): Promise { const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('createBuild').execute('', [{ ...build, _partitionKey: '' }]); + await retry(() => scripts.storedProcedure('createBuild').execute('', [{ ...build, _partitionKey: '' }])); } main().then(() => { diff --git a/build/azure-pipelines/common/extract-telemetry.sh b/build/azure-pipelines/common/extract-telemetry.sh index 6436e93c8c..4abade1e7b 100755 --- a/build/azure-pipelines/common/extract-telemetry.sh +++ b/build/azure-pipelines/common/extract-telemetry.sh @@ -10,8 +10,8 @@ git clone --depth 1 https://github.com/Microsoft/vscode-node-debug2.git git clone --depth 1 https://github.com/Microsoft/vscode-node-debug.git git clone --depth 1 https://github.com/Microsoft/vscode-html-languageservice.git git clone --depth 1 https://github.com/Microsoft/vscode-json-languageservice.git -node $BUILD_SOURCESDIRECTORY/build/node_modules/.bin/vscode-telemetry-extractor --sourceDir $BUILD_SOURCESDIRECTORY --excludedDir $BUILD_SOURCESDIRECTORY/extensions --outputDir . --applyEndpoints -node $BUILD_SOURCESDIRECTORY/build/node_modules/.bin/vscode-telemetry-extractor --config $BUILD_SOURCESDIRECTORY/build/azure-pipelines/common/telemetry-config.json -o . +node $BUILD_SOURCESDIRECTORY/node_modules/.bin/vscode-telemetry-extractor --sourceDir $BUILD_SOURCESDIRECTORY --excludedDir $BUILD_SOURCESDIRECTORY/extensions --outputDir . --applyEndpoints +node $BUILD_SOURCESDIRECTORY/node_modules/.bin/vscode-telemetry-extractor --config $BUILD_SOURCESDIRECTORY/build/azure-pipelines/common/telemetry-config.json -o . mkdir -p $BUILD_SOURCESDIRECTORY/.build/telemetry mv declarations-resolved.json $BUILD_SOURCESDIRECTORY/.build/telemetry/telemetry-core.json mv config-resolved.json $BUILD_SOURCESDIRECTORY/.build/telemetry/telemetry-extensions.json diff --git a/build/azure-pipelines/common/installPlaywright.js b/build/azure-pipelines/common/installPlaywright.js new file mode 100644 index 0000000000..c60f3dc2ad --- /dev/null +++ b/build/azure-pipelines/common/installPlaywright.js @@ -0,0 +1,14 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const retry_1 = require("./retry"); +const { installBrowsersWithProgressBar } = require('playwright/lib/install/installer'); +const playwrightPath = path.dirname(require.resolve('playwright')); +async function install() { + await retry_1.retry(() => installBrowsersWithProgressBar(playwrightPath)); +} +install(); diff --git a/build/azure-pipelines/common/listNodeModules.js b/build/azure-pipelines/common/listNodeModules.js new file mode 100644 index 0000000000..5fad20ec79 --- /dev/null +++ b/build/azure-pipelines/common/listNodeModules.js @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const path = require("path"); +if (process.argv.length !== 3) { + console.error('Usage: node listNodeModules.js OUTPUT_FILE'); + process.exit(-1); +} +const ROOT = path.join(__dirname, '../../../'); +function findNodeModulesFiles(location, inNodeModules, result) { + const entries = fs.readdirSync(path.join(ROOT, location)); + for (const entry of entries) { + const entryPath = `${location}/${entry}`; + if (/(^\/out)|(^\/src$)|(^\/.git$)|(^\/.build$)/.test(entryPath)) { + continue; + } + let stat; + try { + stat = fs.statSync(path.join(ROOT, entryPath)); + } + catch (err) { + continue; + } + if (stat.isDirectory()) { + findNodeModulesFiles(entryPath, inNodeModules || (entry === 'node_modules'), result); + } + else { + if (inNodeModules) { + result.push(entryPath.substr(1)); + } + } + } +} +const result = []; +findNodeModulesFiles('', false, result); +fs.writeFileSync(process.argv[2], result.join('\n') + '\n'); diff --git a/build/azure-pipelines/common/publish-webview.js b/build/azure-pipelines/common/publish-webview.js new file mode 100644 index 0000000000..bf0c3d30c0 --- /dev/null +++ b/build/azure-pipelines/common/publish-webview.js @@ -0,0 +1,71 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const azure = require("azure-storage"); +const mime = require("mime"); +const minimist = require("minimist"); +const path_1 = require("path"); +const fileNames = [ + 'fake.html', + 'host.js', + 'index.html', + 'main.js', + 'service-worker.js' +]; +async function assertContainer(blobService, container) { + await new Promise((c, e) => blobService.createContainerIfNotExists(container, { publicAccessLevel: 'blob' }, err => err ? e(err) : c())); +} +async function doesBlobExist(blobService, container, blobName) { + const existsResult = await new Promise((c, e) => blobService.doesBlobExist(container, blobName, (err, r) => err ? e(err) : c(r))); + return existsResult.exists; +} +async function uploadBlob(blobService, container, blobName, file) { + const blobOptions = { + contentSettings: { + contentType: mime.lookup(file), + cacheControl: 'max-age=31536000, public' + } + }; + await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(container, blobName, file, blobOptions, err => err ? e(err) : c())); +} +async function publish(commit, files) { + console.log('Publishing...'); + console.log('Commit:', commit); + const storageAccount = process.env['AZURE_WEBVIEW_STORAGE_ACCOUNT']; + const blobService = azure.createBlobService(storageAccount, process.env['AZURE_WEBVIEW_STORAGE_ACCESS_KEY']) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + await assertContainer(blobService, commit); + for (const file of files) { + const blobName = path_1.basename(file); + const blobExists = await doesBlobExist(blobService, commit, blobName); + if (blobExists) { + console.log(`Blob ${commit}, ${blobName} already exists, not publishing again.`); + continue; + } + console.log('Uploading blob to Azure storage...'); + await uploadBlob(blobService, commit, blobName, file); + } + console.log('Blobs successfully uploaded.'); +} +function main() { + const commit = process.env['BUILD_SOURCEVERSION']; + if (!commit) { + console.warn('Skipping publish due to missing BUILD_SOURCEVERSION'); + return; + } + const opts = minimist(process.argv.slice(2)); + const [directory] = opts._; + const files = fileNames.map(fileName => path_1.join(directory, fileName)); + publish(commit, files).catch(err => { + console.error(err); + process.exit(1); + }); +} +if (process.argv.length < 3) { + console.error('Usage: node publish.js '); + process.exit(-1); +} +main(); diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js new file mode 100644 index 0000000000..00c6a8ac3f --- /dev/null +++ b/build/azure-pipelines/common/publish.js @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const crypto = require("crypto"); +const azure = require("azure-storage"); +const mime = require("mime"); +const minimist = require("minimist"); +const documentdb_1 = require("documentdb"); +// {{SQL CARBON EDIT}} +if (process.argv.length < 9) { + console.error('Usage: node publish.js [commit_id]'); + process.exit(-1); +} +function hashStream(hashName, stream) { + return new Promise((c, e) => { + const shasum = crypto.createHash(hashName); + stream + .on('data', shasum.update.bind(shasum)) + .on('error', e) + .on('close', () => c(shasum.digest('hex'))); + }); +} +function createDefaultConfig(quality) { + return { + id: quality, + frozen: false + }; +} +function getConfig(quality) { + console.log(`Getting config for quality ${quality}`); + const client = new documentdb_1.DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT'], { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const collection = 'dbs/builds/colls/config'; + const query = { + query: `SELECT TOP 1 * FROM c WHERE c.id = @quality`, + parameters: [ + { name: '@quality', value: quality } + ] + }; + return retry(() => new Promise((c, e) => { + client.queryDocuments(collection, query, { enableCrossPartitionQuery: true }).toArray((err, results) => { + if (err && err.code !== 409) { + return e(err); + } + c(!results || results.length === 0 ? createDefaultConfig(quality) : results[0]); + }); + })); +} +function createOrUpdate(commit, quality, platform, type, release, asset, isUpdate) { + const client = new documentdb_1.DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT'], { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const collection = 'dbs/builds/colls/' + quality; + const updateQuery = { + query: 'SELECT TOP 1 * FROM c WHERE c.id = @id', + parameters: [{ name: '@id', value: commit }] + }; + let updateTries = 0; + function update() { + updateTries++; + return new Promise((c, e) => { + console.log(`Querying existing documents to update...`); + client.queryDocuments(collection, updateQuery, { enableCrossPartitionQuery: true }).toArray((err, results) => { + if (err) { + return e(err); + } + if (results.length !== 1) { + return e(new Error('No documents')); + } + const release = results[0]; + release.assets = [ + ...release.assets.filter((a) => !(a.platform === platform && a.type === type)), + asset + ]; + if (isUpdate) { + release.updates[platform] = type; + } + console.log(`Replacing existing document with updated version`); + client.replaceDocument(release._self, release, err => { + if (err && err.code === 409 && updateTries < 5) { + return c(update()); + } + if (err) { + return e(err); + } + console.log('Build successfully updated.'); + c(); + }); + }); + }); + } + return retry(() => new Promise((c, e) => { + console.log(`Attempting to create document`); + client.createDocument(collection, release, err => { + if (err && err.code === 409) { + return c(update()); + } + if (err) { + return e(err); + } + console.log('Build successfully published.'); + c(); + }); + })); +} +async function assertContainer(blobService, quality) { + await new Promise((c, e) => blobService.createContainerIfNotExists(quality, { publicAccessLevel: 'blob' }, err => err ? e(err) : c())); +} +async function doesAssetExist(blobService, quality, blobName) { + const existsResult = await new Promise((c, e) => blobService.doesBlobExist(quality, blobName, (err, r) => err ? e(err) : c(r))); + return existsResult.exists; +} +async function uploadBlob(blobService, quality, blobName, file) { + const blobOptions = { + contentSettings: { + contentType: mime.lookup(file), + cacheControl: 'max-age=31536000, public' + } + }; + await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, file, blobOptions, err => err ? e(err) : c())); +} +async function publish(commit, quality, platform, type, name, version, _isUpdate, file, opts) { + const isUpdate = _isUpdate === 'true'; + const queuedBy = process.env['BUILD_QUEUEDBY']; + const sourceBranch = process.env['BUILD_SOURCEBRANCH']; + console.log('Publishing...'); + console.log('Quality:', quality); + console.log('Platform:', platform); + console.log('Type:', type); + console.log('Name:', name); + console.log('Version:', version); + console.log('Commit:', commit); + console.log('Is Update:', isUpdate); + console.log('File:', file); + const stat = await new Promise((c, e) => fs.stat(file, (err, stat) => err ? e(err) : c(stat))); + const size = stat.size; + console.log('Size:', size); + const stream = fs.createReadStream(file); + const [sha1hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); + console.log('SHA1:', sha1hash); + console.log('SHA256:', sha256hash); + const blobName = commit + '/' + name; + const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']; + const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + await assertContainer(blobService, quality); + const blobExists = await doesAssetExist(blobService, quality, blobName); + if (blobExists) { + console.log(`Blob ${quality}, ${blobName} already exists, not publishing again.`); + return; + } + console.log('Uploading blobs to Azure storage...'); + await uploadBlob(blobService, quality, blobName, file); + console.log('Blobs successfully uploaded.'); + const config = await getConfig(quality); + console.log('Quality config:', config); + const asset = { + platform: platform, + type: type, + url: `${process.env['AZURE_CDN_URL']}/${quality}/${blobName}`, + hash: sha1hash, + sha256hash, + size + }; + // Remove this if we ever need to rollback fast updates for windows + if (/win32/.test(platform)) { + asset.supportsFastUpdate = true; + } + console.log('Asset:', JSON.stringify(asset, null, ' ')); + // {{SQL CARBON EDIT}} + // Insiders: nightly build from main + const isReleased = (((quality === 'insider' && /^main$|^refs\/heads\/main$/.test(sourceBranch)) || + (quality === 'rc1' && /^release\/|^refs\/heads\/release\//.test(sourceBranch))) && + /Project Collection Service Accounts|Microsoft.VisualStudio.Services.TFS/.test(queuedBy)); + const release = { + id: commit, + timestamp: (new Date()).getTime(), + version, + isReleased: isReleased, + sourceBranch, + queuedBy, + assets: [], + updates: {} + }; + if (!opts['upload-only']) { + release.assets.push(asset); + if (isUpdate) { + release.updates[platform] = type; + } + } + await createOrUpdate(commit, quality, platform, type, release, asset, isUpdate); +} +const RETRY_TIMES = 10; +async function retry(fn) { + for (let run = 1; run <= RETRY_TIMES; run++) { + try { + return await fn(); + } + catch (err) { + if (!/ECONNRESET/.test(err.message)) { + throw err; + } + console.log(`Caught error ${err} - ${run}/${RETRY_TIMES}`); + } + } + throw new Error('Retried too many times'); +} +function main() { + const commit = process.env['BUILD_SOURCEVERSION']; + if (!commit) { + console.warn('Skipping publish due to missing BUILD_SOURCEVERSION'); + return; + } + const opts = minimist(process.argv.slice(2), { + boolean: ['upload-only'] + }); + const [quality, platform, type, name, version, _isUpdate, file] = opts._; + publish(commit, quality, platform, type, name, version, _isUpdate, file, opts).catch(err => { + console.error(err); + process.exit(1); + }); +} +main(); diff --git a/build/azure-pipelines/common/release.js b/build/azure-pipelines/common/release.js new file mode 100644 index 0000000000..0127cd4262 --- /dev/null +++ b/build/azure-pipelines/common/release.js @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const documentdb_1 = require("documentdb"); +function createDefaultConfig(quality) { + return { + id: quality, + frozen: false + }; +} +function getConfig(quality) { + const client = new documentdb_1.DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT'], { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const collection = 'dbs/builds/colls/config'; + const query = { + query: `SELECT TOP 1 * FROM c WHERE c.id = @quality`, + parameters: [ + { name: '@quality', value: quality } + ] + }; + return new Promise((c, e) => { + client.queryDocuments(collection, query).toArray((err, results) => { + if (err && err.code !== 409) { + return e(err); + } + c(!results || results.length === 0 ? createDefaultConfig(quality) : results[0]); + }); + }); +} +function doRelease(commit, quality) { + const client = new documentdb_1.DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT'], { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const collection = 'dbs/builds/colls/' + quality; + const query = { + query: 'SELECT TOP 1 * FROM c WHERE c.id = @id', + parameters: [{ name: '@id', value: commit }] + }; + let updateTries = 0; + function update() { + updateTries++; + return new Promise((c, e) => { + client.queryDocuments(collection, query).toArray((err, results) => { + if (err) { + return e(err); + } + if (results.length !== 1) { + return e(new Error('No documents')); + } + const release = results[0]; + release.isReleased = true; + client.replaceDocument(release._self, release, err => { + if (err && err.code === 409 && updateTries < 5) { + return c(update()); + } + if (err) { + return e(err); + } + console.log('Build successfully updated.'); + c(); + }); + }); + }); + } + return update(); +} +async function release(commit, quality) { + const config = await getConfig(quality); + console.log('Quality config:', config); + if (config.frozen) { + console.log(`Skipping release because quality ${quality} is frozen.`); + return; + } + await doRelease(commit, quality); +} +function env(name) { + const result = process.env[name]; + if (!result) { + throw new Error(`Skipping release due to missing env: ${name}`); + } + return result; +} +async function main() { + const commit = env('BUILD_SOURCEVERSION'); + const quality = env('VSCODE_QUALITY'); + await release(commit, quality); +} +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/common/releaseBuild.js b/build/azure-pipelines/common/releaseBuild.js new file mode 100644 index 0000000000..6932aed3bd --- /dev/null +++ b/build/azure-pipelines/common/releaseBuild.js @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const cosmos_1 = require("@azure/cosmos"); +const retry_1 = require("./retry"); +function getEnv(name) { + const result = process.env[name]; + if (typeof result === 'undefined') { + throw new Error('Missing env: ' + name); + } + return result; +} +function createDefaultConfig(quality) { + return { + id: quality, + frozen: false + }; +} +async function getConfig(client, quality) { + const query = `SELECT TOP 1 * FROM c WHERE c.id = "${quality}"`; + const res = await client.database('builds').container('config').items.query(query).fetchAll(); + if (res.resources.length === 0) { + return createDefaultConfig(quality); + } + return res.resources[0]; +} +async function main() { + const commit = getEnv('BUILD_SOURCEVERSION'); + const quality = getEnv('VSCODE_QUALITY'); + const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const config = await getConfig(client, quality); + console.log('Quality config:', config); + if (config.frozen) { + console.log(`Skipping release because quality ${quality} is frozen.`); + return; + } + console.log(`Releasing build ${commit}...`); + const scripts = client.database('builds').container(quality).scripts; + await retry_1.retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); +} +main().then(() => { + console.log('Build successfully released'); + process.exit(0); +}, err => { + console.error(err); + process.exit(1); +}); diff --git a/build/azure-pipelines/common/releaseBuild.ts b/build/azure-pipelines/common/releaseBuild.ts index e8a9835fb6..7e593e5989 100644 --- a/build/azure-pipelines/common/releaseBuild.ts +++ b/build/azure-pipelines/common/releaseBuild.ts @@ -6,6 +6,7 @@ 'use strict'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; function getEnv(name: string): string { const result = process.env[name]; @@ -58,7 +59,7 @@ async function main(): Promise { console.log(`Releasing build ${commit}...`); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('releaseBuild').execute('', [commit]); + await retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); } main().then(() => { diff --git a/build/azure-pipelines/common/retry.js b/build/azure-pipelines/common/retry.js new file mode 100644 index 0000000000..6735d4ae15 --- /dev/null +++ b/build/azure-pipelines/common/retry.js @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.retry = void 0; +async function retry(fn) { + for (let run = 1; run <= 10; run++) { + try { + return await fn(); + } + catch (err) { + if (!/ECONNRESET/.test(err.message)) { + throw err; + } + const millis = (Math.random() * 200) + (50 * Math.pow(1.5, run)); + console.log(`Failed with ECONNRESET, retrying in ${millis}ms...`); + // maximum delay is 10th retry: ~3 seconds + await new Promise(c => setTimeout(c, millis)); + } + } + throw new Error('Retried too many times'); +} +exports.retry = retry; diff --git a/build/azure-pipelines/common/retry.ts b/build/azure-pipelines/common/retry.ts new file mode 100644 index 0000000000..23a2535696 --- /dev/null +++ b/build/azure-pipelines/common/retry.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export async function retry(fn: () => Promise): Promise { + for (let run = 1; run <= 10; run++) { + try { + return await fn(); + } catch (err) { + if (!/ECONNRESET/.test(err.message)) { + throw err; + } + + const millis = (Math.random() * 200) + (50 * Math.pow(1.5, run)); + console.log(`Failed with ECONNRESET, retrying in ${millis}ms...`); + + // maximum delay is 10th retry: ~3 seconds + await new Promise(c => setTimeout(c, millis)); + } + } + + throw new Error('Retried too many times'); +} diff --git a/build/azure-pipelines/common/sync-mooncake.js b/build/azure-pipelines/common/sync-mooncake.js new file mode 100644 index 0000000000..d5a4445223 --- /dev/null +++ b/build/azure-pipelines/common/sync-mooncake.js @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const url = require("url"); +const azure = require("azure-storage"); +const mime = require("mime"); +const cosmos_1 = require("@azure/cosmos"); +const retry_1 = require("./retry"); +function log(...args) { + console.log(...[`[${new Date().toISOString()}]`, ...args]); +} +function error(...args) { + console.error(...[`[${new Date().toISOString()}]`, ...args]); +} +if (process.argv.length < 3) { + error('Usage: node sync-mooncake.js '); + process.exit(-1); +} +async function sync(commit, quality) { + log(`Synchronizing Mooncake assets for ${quality}, ${commit}...`); + const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); + const container = client.database('builds').container(quality); + const query = `SELECT TOP 1 * FROM c WHERE c.id = "${commit}"`; + const res = await container.items.query(query, {}).fetchAll(); + if (res.resources.length !== 1) { + throw new Error(`No builds found for ${commit}`); + } + const build = res.resources[0]; + log(`Found build for ${commit}, with ${build.assets.length} assets`); + const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']; + const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + const mooncakeBlobService = azure.createBlobService(storageAccount, process.env['MOONCAKE_STORAGE_ACCESS_KEY'], `${storageAccount}.blob.core.chinacloudapi.cn`) + .withFilter(new azure.ExponentialRetryPolicyFilter(20)); + // mooncake is fussy and far away, this is needed! + blobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + mooncakeBlobService.defaultClientRequestTimeoutInMs = 10 * 60 * 1000; + for (const asset of build.assets) { + try { + const blobPath = url.parse(asset.url).path; + if (!blobPath) { + throw new Error(`Failed to parse URL: ${asset.url}`); + } + const blobName = blobPath.replace(/^\/\w+\//, ''); + log(`Found ${blobName}`); + if (asset.mooncakeUrl) { + log(` Already in Mooncake ✔️`); + continue; + } + const readStream = blobService.createReadStream(quality, blobName, undefined); + const blobOptions = { + contentSettings: { + contentType: mime.lookup(blobPath), + cacheControl: 'max-age=31536000, public' + } + }; + const writeStream = mooncakeBlobService.createWriteStreamToBlockBlob(quality, blobName, blobOptions, undefined); + log(` Uploading to Mooncake...`); + await new Promise((c, e) => readStream.pipe(writeStream).on('finish', c).on('error', e)); + log(` Updating build in DB...`); + const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; + await retry_1.retry(() => container.scripts.storedProcedure('setAssetMooncakeUrl') + .execute('', [commit, asset.platform, asset.type, mooncakeUrl])); + log(` Done ✔️`); + } + catch (err) { + error(err); + } + } + log(`All done ✔️`); +} +function main() { + const commit = process.env['BUILD_SOURCEVERSION']; + if (!commit) { + error('Skipping publish due to missing BUILD_SOURCEVERSION'); + return; + } + const quality = process.argv[2]; + sync(commit, quality).catch(err => { + error(err); + process.exit(1); + }); +} +main(); diff --git a/build/azure-pipelines/common/sync-mooncake.ts b/build/azure-pipelines/common/sync-mooncake.ts index 97fc67c10f..aa645a8861 100644 --- a/build/azure-pipelines/common/sync-mooncake.ts +++ b/build/azure-pipelines/common/sync-mooncake.ts @@ -9,6 +9,7 @@ import * as url from 'url'; import * as azure from 'azure-storage'; import * as mime from 'mime'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; function log(...args: any[]) { console.log(...[`[${new Date().toISOString()}]`, ...args]); @@ -99,8 +100,8 @@ async function sync(commit: string, quality: string): Promise { log(` Updating build in DB...`); const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; - await container.scripts.storedProcedure('setAssetMooncakeUrl') - .execute('', [commit, asset.platform, asset.type, mooncakeUrl]); + await retry(() => container.scripts.storedProcedure('setAssetMooncakeUrl') + .execute('', [commit, asset.platform, asset.type, mooncakeUrl])); log(` Done ✔️`); } catch (err) { diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index bf7cf874fd..4f99a15ef0 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 # {{SQL CARBON EDIT}} update version inputs: diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist deleted file mode 100644 index 7cd9df032b..0000000000 --- a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-library-validation - - - diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 6f7dec16ac..a34ae1c424 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -1,32 +1,7 @@ steps: - - script: | - mkdir -p .build - echo -n $BUILD_SOURCEVERSION > .build/commit - echo -n $VSCODE_QUALITY > .build/quality - echo -n $ENABLE_TERRAPIN > .build/terrapin - displayName: Prepare compilation cache flags - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - - - script: | - set -e - exit 1 - displayName: Check RestoreCache - condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true')) - - task: NodeTool@0 inputs: - versionSpec: "12.14.1" - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - inputs: - versionSpec: "1.x" + versionSpec: "12.18.3" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -34,9 +9,21 @@ steps: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) + - script: | set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) + - script: | + set -e cat << EOF > ~/.netrc machine github.com login vscode @@ -55,40 +42,45 @@ steps: - script: | set -e - git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" - git fetch distro - git merge $(node -p "require('./package.json').distro") + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") displayName: Merge distro - script: | - npx https://aka.ms/enablesecurefeed standAlone - displayName: Switch to Terrapin packages - timeoutInMinutes: 5 - condition: and(succeeded(), eq(variables['ENABLE_TERRAPIN'], 'true')) - - - script: | - echo -n $(VSCODE_ARCH) > .build/arch + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js $VSCODE_ARCH $ENABLE_TERRAPIN > .build/yarnlockhash displayName: Prepare yarn cache flags - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + - task: Cache@2 inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" + key: 'nodeModules | $(Agent.OS) | .build/yarnlockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache - script: | set -e - npm install -g node-gyp@7.1.0 + tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + set -e + npm install -g node-gyp@latest node-gyp --version displayName: Update node-gyp - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - script: | + 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 - script: | set -e export npm_config_arch=$(VSCODE_ARCH) export npm_config_node_gyp=$(which node-gyp) export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk - export CHILD_CONCURRENCY="1" for i in {1..3}; do # try 3 times, for Terrapin yarn --frozen-lockfile && break @@ -98,25 +90,19 @@ steps: fi echo "Yarn failed $i, trying again..." done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 displayName: Install dependencies - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | set -e - export npm_config_arch=$(VSCODE_ARCH) - export npm_config_node_gyp=$(which node-gyp) - export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk - ls /Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/ - yarn postinstall - displayName: Run postinstall scripts - condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive - script: | set -e @@ -126,10 +112,21 @@ steps: export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk ls /Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/ yarn electron-rebuild + # remove once https://github.com/prebuild/prebuild-install/pull/140 is merged and found in keytar cd ./node_modules/keytar node-gyp rebuild displayName: Rebuild native modules for ARM64 - condition: eq(variables['VSCODE_ARCH'], 'arm64') + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64')) + + - download: current + artifact: vscode-darwin-x64 + displayName: Download x64 artifact + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) + + - download: current + artifact: vscode-darwin-arm64 + displayName: Download arm64 artifact + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - script: | set -e @@ -141,6 +138,7 @@ steps: VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ yarn gulp vscode-darwin-$(VSCODE_ARCH)-min-ci displayName: Build + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) - script: | set -e @@ -148,14 +146,23 @@ steps: yarn gulp vscode-reh-darwin-min-ci VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ yarn gulp vscode-reh-web-darwin-min-ci - displayName: Build reh + displayName: Build Server condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) - script: | set -e - yarn electron $(VSCODE_ARCH) - displayName: Download Electron - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + unzip $(Pipeline.Workspace)/vscode-darwin-x64/VSCode-darwin-x64.zip -d $(agent.builddirectory)/vscode-x64 + unzip $(Pipeline.Workspace)/vscode-darwin-arm64/VSCode-darwin-arm64.zip -d $(agent.builddirectory)/vscode-arm64 + DEBUG=* node build/darwin/create-universal-app.js + displayName: Create Universal App + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) + + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + displayName: Download Electron and Playwright + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e @@ -167,17 +174,26 @@ steps: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain VSCODE_ARCH="$(VSCODE_ARCH)" DEBUG=electron-osx-sign* node build/darwin/sign.js displayName: Set Hardened Entitlements + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e ./scripts/test.sh --build --tfs "Unit Tests" displayName: Run unit tests (Electron) + timeoutInMinutes: 5 condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e yarn test-browser --build --browser chromium --browser webkit --browser firefox --tfs "Browser Unit Tests" displayName: Run unit tests (Browser) + timeoutInMinutes: 5 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + + - script: | + set -e + yarn --cwd test/integration/browser compile + displayName: Compile integration tests condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | @@ -191,13 +207,15 @@ steps: VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ ./scripts/test-integration.sh --build --tfs "Integration Tests" displayName: Run integration tests (Electron) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + timeoutInMinutes: 10 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ ./resources/server/test/test-web-integration.sh --browser webkit displayName: Run integration tests (Browser) + timeoutInMinutes: 5 condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | @@ -208,22 +226,39 @@ steps: VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ ./resources/server/test/test-remote-integration.sh displayName: Run remote integration tests (Electron) + timeoutInMinutes: 5 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + + - script: | + set -e + yarn --cwd test/smoke compile + displayName: Compile smoke tests condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" - yarn smoketest --build "$APP_ROOT/$APP_NAME" - continueOnError: true + yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" + timeoutInMinutes: 5 displayName: Run smoke tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ + yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --remote + timeoutInMinutes: 5 + displayName: Run smoke tests (Remote) + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ - yarn smoketest --web --headless - continueOnError: true + yarn smoketest-no-compile --web --headless + timeoutInMinutes: 5 displayName: Run smoke tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) @@ -240,12 +275,13 @@ steps: inputs: testResultsFiles: "*-results.xml" searchFolder: "$(Build.ArtifactStagingDirectory)/test-results" - condition: succeededOrFailed() + condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip * && popd displayName: Archive build + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: @@ -270,10 +306,12 @@ steps: ] SessionTimeout: 60 displayName: Codesign + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | zip -d $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip "*.pkg" - displayName: Clean Archive + displayName: Clean + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) @@ -281,6 +319,7 @@ steps: BUNDLE_IDENTIFIER=$(node -p "require(\"$APP_ROOT/$APP_NAME/Contents/Resources/app/product.json\").darwinBundleIdentifier") echo "##vso[task.setvariable variable=BundleIdentifier]$BUNDLE_IDENTIFIER" displayName: Export bundle identifier + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: @@ -305,6 +344,7 @@ steps: ] SessionTimeout: 60 displayName: Notarization + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e @@ -312,7 +352,7 @@ steps: APP_NAME="`ls $APP_ROOT | head -n 1`" "$APP_ROOT/$APP_NAME/Contents/Resources/app/bin/code" --export-default-configuration=.build displayName: Verify start after signing (export configuration) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64'), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e @@ -323,13 +363,29 @@ steps: VSCODE_ARCH="$(VSCODE_ARCH)" \ ./build/azure-pipelines/darwin/publish.sh displayName: Publish + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip + artifact: vscode-darwin-$(VSCODE_ARCH) + displayName: Publish archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/vscode-server-darwin.zip + artifact: vscode-server-darwin-$(VSCODE_ARCH) + displayName: Publish server archive + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/vscode-server-darwin-web.zip + artifact: vscode-server-darwin-$(VSCODE_ARCH)-web + displayName: Publish web server archive + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ VSCODE_ARCH="$(VSCODE_ARCH)" \ yarn gulp upload-vscode-configuration displayName: Upload configuration (for Bing settings search) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + 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 diff --git a/build/azure-pipelines/darwin/publish.sh b/build/azure-pipelines/darwin/publish.sh index c9f5b4bab5..df5b9770c1 100755 --- a/build/azure-pipelines/darwin/publish.sh +++ b/build/azure-pipelines/darwin/publish.sh @@ -5,6 +5,7 @@ set -e case $VSCODE_ARCH in x64) ASSET_ID="darwin" ;; arm64) ASSET_ID="darwin-arm64" ;; + universal) ASSET_ID="darwin-universal" ;; esac # publish the build diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index cf519bbb03..08aa145de9 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -11,7 +11,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: AzureKeyVault@1 displayName: 'Azure Key Vault: Get Secrets' diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml index fb9cecc12e..10b83a8c17 100644 --- a/build/azure-pipelines/exploration-build.yml +++ b/build/azure-pipelines/exploration-build.yml @@ -11,7 +11,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/linux/Dockerfile b/build/azure-pipelines/linux/Dockerfile index b070bf75fa..cd79fdff9e 100644 --- a/build/azure-pipelines/linux/Dockerfile +++ b/build/azure-pipelines/linux/Dockerfile @@ -6,12 +6,12 @@ RUN apt-get update && apt-get upgrade -y RUN apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 \ libkrb5-dev git apt-transport-https ca-certificates curl gnupg-agent software-properties-common \ - libnss3 libasound2 make gcc libx11-dev fakeroot rpm libgconf-2-4 libunwind8 g++-4.8 + libnss3 libasound2 make gcc libx11-dev fakeroot rpm libgconf-2-4 libunwind8 g++-4.9 RUN rm /usr/bin/gcc RUN rm /usr/bin/g++ -RUN ln -s /usr/bin/gcc-4.8 /usr/bin/gcc -RUN ln -s /usr/bin/g++-4.8 /usr/bin/g++ +RUN ln -s /usr/bin/gcc-4.9 /usr/bin/gcc +RUN ln -s /usr/bin/g++-4.9 /usr/bin/g++ #docker RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - diff --git a/build/azure-pipelines/linux/alpine/publish.sh b/build/azure-pipelines/linux/alpine/publish.sh index 4bf874c63c..2f5647d1ea 100755 --- a/build/azure-pipelines/linux/alpine/publish.sh +++ b/build/azure-pipelines/linux/alpine/publish.sh @@ -22,7 +22,7 @@ SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX-web" SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX-web.tar.gz" SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" -rm -rf $ROOT/vscode-server-*.tar.* +rm -rf $ROOT/vscode-server-*-web.tar.* (cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX-web" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index a97dcdc593..fbd073717b 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -10,16 +10,21 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: versionSpec: "1.x" + - script: | + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js > .build/yarnlockhash + displayName: Prepare yarn cache flags + - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 displayName: Restore Cache - Node Modules # {{SQL CARBON EDIT}} inputs: - keyfile: "build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock" + keyfile: ".yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" targetfolder: "**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules" vstsFeed: "npm-cache" # {{SQL CARBON EDIT}} update build cache @@ -31,7 +36,7 @@ steps: - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 displayName: Save Cache - Node Modules # {{SQL CARBON EDIT}} inputs: - keyfile: "build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock" + keyfile: ".yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" targetfolder: "**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules" vstsFeed: "npm-cache" # {{SQL CARBON EDIT}} update build cache condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index 9a54bc2bc8..a2bbb119bf 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -1,28 +1,7 @@ steps: - - script: | - mkdir -p .build - echo -n $BUILD_SOURCEVERSION > .build/commit - echo -n $VSCODE_QUALITY > .build/quality - echo -n $ENABLE_TERRAPIN > .build/terrapin - displayName: Prepare compilation cache flags - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - - - script: | - set -e - exit 1 - displayName: Check RestoreCache - condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true')) - - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -34,6 +13,17 @@ steps: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - script: | + set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output + - task: Docker@1 displayName: "Pull image" inputs: @@ -45,7 +35,6 @@ steps: - script: | set -e - cat << EOF > ~/.netrc machine github.com login vscode @@ -58,30 +47,35 @@ steps: - script: | set -e - git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" - git fetch distro - git merge $(node -p "require('./package.json').distro") + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") displayName: Merge distro - script: | - npx https://aka.ms/enablesecurefeed standAlone - displayName: Switch to Terrapin packages - timeoutInMinutes: 5 - condition: and(succeeded(), eq(variables['ENABLE_TERRAPIN'], 'true')) - - - script: | - echo -n "alpine" > .build/arch + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js "alpine" $ENABLE_TERRAPIN > .build/yarnlockhash displayName: Prepare yarn cache flags - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + - task: Cache@2 inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" + key: 'nodeModules | $(Agent.OS) | .build/yarnlockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - script: | + set -e + tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + 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 - script: | set -e - export CHILD_CONCURRENCY="1" for i in {1..3}; do # try 3 times, for Terrapin yarn --frozen-lockfile && break if [ $i -eq 3 ]; then @@ -90,21 +84,19 @@ steps: fi echo "Yarn failed $i, trying again..." done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 displayName: Install dependencies - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | set -e - yarn postinstall - displayName: Run postinstall scripts - condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive - script: | set -e @@ -113,7 +105,7 @@ steps: - script: | set -e - docker run -e VSCODE_QUALITY -e CHILD_CONCURRENCY=1 -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:alpine /root/vscode/build/azure-pipelines/linux/alpine/install-dependencies.sh + docker run -e VSCODE_QUALITY -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:alpine /root/vscode/build/azure-pipelines/linux/alpine/install-dependencies.sh displayName: Prebuild - script: | @@ -129,6 +121,17 @@ steps: VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ ./build/azure-pipelines/linux/alpine/publish.sh displayName: Publish + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine.tar.gz + artifact: vscode-server-linux-alpine + displayName: Publish server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/vscode-server-linux-alpine-web.tar.gz + 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" diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 1e88a0a3b5..9a42f05815 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -1,28 +1,7 @@ steps: - - script: | - mkdir -p .build - echo -n $BUILD_SOURCEVERSION > .build/commit - echo -n $VSCODE_QUALITY > .build/quality - echo -n $ENABLE_TERRAPIN > .build/terrapin - displayName: Prepare compilation cache flags - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - - - script: | - set -e - exit 1 - displayName: Check RestoreCache - condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true')) - - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -34,6 +13,17 @@ steps: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - script: | + set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output + - script: | set -e cat << EOF > ~/.netrc @@ -48,31 +38,56 @@ steps: - script: | set -e - git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" - git fetch distro - git merge $(node -p "require('./package.json').distro") + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") displayName: Merge distro - script: | - npx https://aka.ms/enablesecurefeed standAlone - displayName: Switch to Terrapin packages - timeoutInMinutes: 5 - condition: and(succeeded(), eq(variables['ENABLE_TERRAPIN'], 'true')) - - - script: | - echo -n $(VSCODE_ARCH) > .build/arch + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js $VSCODE_ARCH $ENABLE_TERRAPIN > .build/yarnlockhash displayName: Prepare yarn cache flags - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + - task: Cache@2 inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" + key: 'nodeModules | $(Agent.OS) | .build/yarnlockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - script: | + set -e + tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + set -e + npm install -g node-gyp@latest + node-gyp --version + displayName: Update node-gyp + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['VSCODE_ARCH'], 'x64')) + + - script: | + 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 - script: | set -e export npm_config_arch=$(NPM_ARCH) - export CHILD_CONCURRENCY="1" + export npm_config_build_from_source=true + + if [ -z "$CC" ] || [ -z "$CXX" ]; then + export CC=$(which gcc-5) + export CXX=$(which g++-5) + fi + + if [ "$VSCODE_ARCH" == "x64" ]; then + export VSCODE_REMOTE_CC=$(which gcc-4.8) + export VSCODE_REMOTE_CXX=$(which g++-4.8) + export VSCODE_REMOTE_NODE_GYP=$(which node-gyp) + fi + for i in {1..3}; do # try 3 times, for Terrapin yarn --frozen-lockfile && break if [ $i -eq 3 ]; then @@ -81,21 +96,23 @@ steps: fi echo "Yarn failed $i, trying again..." done - displayName: Install dependencies - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + # remove once https://github.com/prebuild/prebuild-install/pull/140 is merged and found in keytar + cd ./node_modules/keytar + npx node-gyp rebuild + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | set -e - yarn postinstall - displayName: Run postinstall scripts - condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive - script: | set -e @@ -106,28 +123,41 @@ steps: set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ yarn gulp vscode-linux-$(VSCODE_ARCH)-min-ci - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci displayName: Build - script: | set -e - service xvfb start - displayName: Start xvfb + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci + displayName: Build Server + + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + displayName: Download Electron and Playwright condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" displayName: Run unit tests (Electron) + timeoutInMinutes: 5 condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e DISPLAY=:10 yarn test-browser --build --browser chromium --tfs "Browser Unit Tests" displayName: Run unit tests (Browser) + timeoutInMinutes: 5 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + + - script: | + set -e + yarn --cwd test/integration/browser compile + displayName: Compile integration tests condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | @@ -137,10 +167,12 @@ steps: set -e APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_APP_NAME="$APP_NAME" \ INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests" displayName: Run integration tests (Electron) + timeoutInMinutes: 10 condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | @@ -148,16 +180,19 @@ steps: VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium displayName: Run integration tests (Browser) + timeoutInMinutes: 5 condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_APP_NAME="$APP_NAME" \ INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ DISPLAY=:10 ./resources/server/test/test-remote-integration.sh displayName: Run remote integration tests (Electron) + timeoutInMinutes: 5 condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: PublishPipelineArtifact@0 @@ -180,17 +215,20 @@ steps: yarn gulp "vscode-linux-$(VSCODE_ARCH)-build-deb" yarn gulp "vscode-linux-$(VSCODE_ARCH)-build-rpm" displayName: Build deb, rpm packages + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e yarn gulp "vscode-linux-$(VSCODE_ARCH)-prepare-snap" displayName: Prepare snap package + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) # needed for code signing - task: UseDotNet@2 displayName: "Install .NET Core SDK 2.x" inputs: version: 2.x + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: @@ -210,6 +248,7 @@ steps: ] SessionTimeout: 120 displayName: Codesign rpm + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e @@ -219,12 +258,34 @@ steps: VSCODE_ARCH="$(VSCODE_ARCH)" \ ./build/azure-pipelines/linux/publish.sh displayName: Publish + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(DEB_PATH) + artifact: vscode-linux-deb-$(VSCODE_ARCH) + displayName: Publish deb package + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(RPM_PATH) + artifact: vscode-linux-rpm-$(VSCODE_ARCH) + displayName: Publish rpm package + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH).tar.gz + artifact: vscode-server-linux-$(VSCODE_ARCH) + displayName: Publish server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web.tar.gz + artifact: vscode-server-linux-$(VSCODE_ARCH)-web + displayName: Publish web server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: PublishPipelineArtifact@0 displayName: "Publish Pipeline Artifact" inputs: 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" diff --git a/build/azure-pipelines/linux/publish.sh b/build/azure-pipelines/linux/publish.sh index e0f1ade30d..6d748c6e34 100755 --- a/build/azure-pipelines/linux/publish.sh +++ b/build/azure-pipelines/linux/publish.sh @@ -26,6 +26,17 @@ rm -rf $ROOT/vscode-server-*.tar.* node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" +# Publish Remote Extension Host (Web) +LEGACY_SERVER_BUILD_NAME="vscode-reh-web-$PLATFORM_LINUX" +SERVER_BUILD_NAME="vscode-server-$PLATFORM_LINUX-web" +SERVER_TARBALL_FILENAME="vscode-server-$PLATFORM_LINUX-web.tar.gz" +SERVER_TARBALL_PATH="$ROOT/$SERVER_TARBALL_FILENAME" + +rm -rf $ROOT/vscode-server-*-web.tar.* +(cd $ROOT && mv $LEGACY_SERVER_BUILD_NAME $SERVER_BUILD_NAME && tar --owner=0 --group=0 -czf $SERVER_TARBALL_PATH $SERVER_BUILD_NAME) + +node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX-web" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" + # Publish DEB case $VSCODE_ARCH in x64) DEB_ARCH="amd64" ;; @@ -58,3 +69,7 @@ mkdir -p $REPO/.build/linux/snap-tarball SNAP_TARBALL_PATH="$REPO/.build/linux/snap-tarball/snap-$VSCODE_ARCH.tar.gz" rm -rf $SNAP_TARBALL_PATH (cd .build/linux && tar -czf $SNAP_TARBALL_PATH snap) + +# Export DEB_PATH, RPM_PATH +echo "##vso[task.setvariable variable=DEB_PATH]$DEB_PATH" +echo "##vso[task.setvariable variable=RPM_PATH]$RPM_PATH" diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index f08c7b3c3e..09c5488f22 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -54,3 +54,11 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ node build/azure-pipelines/common/createAsset.js "linux-snap-$(VSCODE_ARCH)" package "$SNAP_FILENAME" "$SNAP_PATH" + + # Export SNAP_PATH + echo "##vso[task.setvariable variable=SNAP_PATH]$SNAP_PATH" + + - publish: $(SNAP_PATH) + artifact: vscode-linux-snap-$(VSCODE_ARCH) + displayName: Publish snap package + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/linux/sql-product-build-linux.yml b/build/azure-pipelines/linux/sql-product-build-linux.yml index 9dcfffec09..668880a958 100644 --- a/build/azure-pipelines/linux/sql-product-build-linux.yml +++ b/build/azure-pipelines/linux/sql-product-build-linux.yml @@ -131,16 +131,19 @@ steps: displayName: Run integration tests (Electron) condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - - ${{each extension in parameters.extensionsToUnitTest}}: - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/azuredatastudio-linux-x64 - APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") - export INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ - export NO_CLEANUP=1 - DISPLAY=:10 node ./scripts/test-extensions-unit.js ${{ extension }} - displayName: 'Run ${{ extension }} Stable Extension Unit Tests' - condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) + - script: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the unit tests + # to run with these builds instead of running out of sources. + set -e + APP_ROOT=$(agent.builddirectory)/azuredatastudio-linux-x64 + APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + NO_CLEANUP=1 \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-linux-x64" \ + DISPLAY=:10 ./scripts/test-extensions-unit.sh --build --tfs "Extension Unit Tests" + displayName: 'Run Stable Extension Unit Tests' + condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true')) - script: | set -e @@ -162,7 +165,6 @@ steps: # Only archive directories we want for debugging purposes tar -czvf $(Build.ArtifactStagingDirectory)/logs/linux-x64/$folder.tar.gz $folder/User $folder/logs done - displayName: Archive Logs continueOnError: true condition: succeededOrFailed() @@ -237,6 +239,13 @@ steps: continueOnError: true condition: and(succeededOrFailed(), eq(variables['RUN_TESTS'], 'true')) + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: crash reports' + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/.build/crashes' + ArtifactName: crashes + condition: and(succeededOrFailed(), eq(variables['RUN_TESTS'], 'true')) + - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: drop' condition: succeededOrFailed() diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index ebe2fe6abe..4d37624694 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -1,4 +1,3 @@ -trigger: none pr: none schedules: @@ -8,11 +7,108 @@ schedules: include: - master +parameters: + - name: VSCODE_QUALITY + displayName: Quality + type: string + default: insider + values: + - exploration + - insider + - stable + - name: ENABLE_TERRAPIN + displayName: "Enable Terrapin" + type: boolean + default: true + - name: VSCODE_BUILD_WIN32 + displayName: "🎯 Windows x64" + type: boolean + default: true + - name: VSCODE_BUILD_WIN32_32BIT + displayName: "🎯 Windows ia32" + type: boolean + default: true + - name: VSCODE_BUILD_WIN32_ARM64 + displayName: "🎯 Windows arm64" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX + displayName: "🎯 Linux x64" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX_ARM64 + displayName: "🎯 Linux arm64" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX_ARMHF + displayName: "🎯 Linux armhf" + type: boolean + default: true + - name: VSCODE_BUILD_LINUX_ALPINE + displayName: "🎯 Alpine Linux" + type: boolean + default: true + - name: VSCODE_BUILD_MACOS + displayName: "🎯 macOS x64" + type: boolean + default: true + - name: VSCODE_BUILD_MACOS_ARM64 + displayName: "🎯 macOS arm64" + type: boolean + default: true + - name: VSCODE_BUILD_MACOS_UNIVERSAL + displayName: "🎯 macOS universal" + type: boolean + default: true + - name: VSCODE_BUILD_WEB + displayName: "🎯 Web" + type: boolean + default: true + - name: VSCODE_PUBLISH + displayName: "Publish to builds.code.visualstudio.com" + type: boolean + default: true + - name: VSCODE_RELEASE + displayName: "Release build if successful" + type: boolean + default: false + - name: VSCODE_COMPILE_ONLY + displayName: "Run Compile stage exclusively" + type: boolean + default: false + - name: VSCODE_STEP_ON_IT + displayName: "Skip tests" + type: boolean + default: false + +variables: + - name: ENABLE_TERRAPIN + value: ${{ eq(parameters.ENABLE_TERRAPIN, true) }} + - name: VSCODE_QUALITY + value: ${{ parameters.VSCODE_QUALITY }} + - name: VSCODE_BUILD_STAGE_WINDOWS + value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_32BIT, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} + - name: VSCODE_BUILD_STAGE_LINUX + value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true), eq(parameters.VSCODE_BUILD_WEB, true)) }} + - name: VSCODE_BUILD_STAGE_MACOS + value: ${{ or(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }} + - name: VSCODE_CIBUILD + value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }} + - name: VSCODE_PUBLISH + value: ${{ and(eq(parameters.VSCODE_PUBLISH, true), eq(variables.VSCODE_CIBUILD, false)) }} + - name: VSCODE_SCHEDULEDBUILD + value: ${{ eq(variables['Build.Reason'], 'Schedule') }} + - name: VSCODE_STEP_ON_IT + value: ${{ eq(parameters.VSCODE_STEP_ON_IT, true) }} + - name: VSCODE_BUILD_MACOS_UNIVERSAL + value: ${{ and(eq(variables['VSCODE_PUBLISH'], true), eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true), eq(parameters.VSCODE_BUILD_MACOS_UNIVERSAL, true)) }} + resources: containers: - container: vscode-x64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:x64 + image: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-x64 endpoint: VSCodeHub + options: --user 0:0 - container: vscode-arm64 image: vscodehub.azurecr.io/vscode-linux-build-agent:stretch-arm64 endpoint: VSCodeHub @@ -26,177 +122,189 @@ stages: - stage: Compile jobs: - job: Compile - pool: - vmImage: "Ubuntu-16.04" - container: vscode-x64 + pool: compile variables: VSCODE_ARCH: x64 steps: - template: product-compile.yml - - stage: Windows - dependsOn: - - Compile - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) - pool: - vmImage: VS2017-Win2016 - jobs: - - job: Windows - condition: and(succeeded(), eq(variables['VSCODE_BUILD_WIN32'], 'true')) - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true)) }}: + - stage: Windows + dependsOn: + - Compile + pool: + vmImage: VS2017-Win2016 + jobs: - - job: Windows32 - condition: and(succeeded(), eq(variables['VSCODE_BUILD_WIN32_32BIT'], 'true')) - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: ia32 - steps: - - template: win32/product-build-win32.yml + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - job: Windows + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: win32/product-build-win32.yml - - job: WindowsARM64 - condition: and(succeeded(), eq(variables['VSCODE_BUILD_WIN32_ARM64'], 'true')) - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: arm64 - steps: - - template: win32/product-build-win32.yml + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_32BIT, true)) }}: + - job: Windows32 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: ia32 + steps: + - template: win32/product-build-win32.yml - - stage: Linux - dependsOn: - - Compile - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) - pool: - vmImage: "Ubuntu-16.04" - jobs: - - job: Linux - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX'], 'true')) - container: vscode-x64 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - steps: - - template: linux/product-build-linux.yml + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: + - job: WindowsARM64 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: arm64 + steps: + - template: win32/product-build-win32.yml - - job: LinuxSnap - dependsOn: + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX'], true)) }}: + - stage: Linux + dependsOn: + - Compile + pool: + vmImage: "Ubuntu-18.04" + jobs: + + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - job: Linux + container: vscode-x64 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + steps: + - template: linux/product-build-linux.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}: + - job: LinuxSnap + dependsOn: + - Linux + container: snapcraft + variables: + VSCODE_ARCH: x64 + steps: + - template: linux/snap-build-linux.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: + - job: LinuxArmhf + container: vscode-armhf + variables: + VSCODE_ARCH: armhf + NPM_ARCH: armv7l + steps: + - template: linux/product-build-linux.yml + + # TODO@joaomoreno: We don't ship ARM snaps for now + - ${{ if and(false, eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true)) }}: + - job: LinuxSnapArmhf + dependsOn: + - LinuxArmhf + container: snapcraft + variables: + VSCODE_ARCH: armhf + steps: + - template: linux/snap-build-linux.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: + - job: LinuxArm64 + container: vscode-arm64 + variables: + VSCODE_ARCH: arm64 + NPM_ARCH: arm64 + steps: + - template: linux/product-build-linux.yml + + # TODO@joaomoreno: We don't ship ARM snaps for now + - ${{ if and(false, eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: + - job: LinuxSnapArm64 + dependsOn: + - LinuxArm64 + container: snapcraft + variables: + VSCODE_ARCH: arm64 + steps: + - template: linux/snap-build-linux.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true)) }}: + - job: LinuxAlpine + steps: + - template: linux/product-build-alpine.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WEB, true)) }}: + - job: LinuxWeb + variables: + VSCODE_ARCH: x64 + steps: + - template: web/product-build-web.yml + + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_MACOS'], true)) }}: + - stage: macOS + dependsOn: + - Compile + pool: + vmImage: macOS-latest + jobs: + + - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: + - job: macOS + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: darwin/product-build-darwin.yml + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: + - job: macOSARM64 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: arm64 + steps: + - template: darwin/product-build-darwin.yml + + - ${{ if eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true) }}: + - job: macOSUniversal + dependsOn: + - macOS + - macOSARM64 + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: universal + steps: + - template: darwin/product-build-darwin.yml + + - ${{ if and(eq(variables['VSCODE_PUBLISH'], true), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: + - stage: Mooncake + dependsOn: + - ${{ if eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true) }}: + - Windows + - ${{ if eq(variables['VSCODE_BUILD_STAGE_LINUX'], true) }}: - Linux - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX'], 'true')) - container: snapcraft - variables: - VSCODE_ARCH: x64 - steps: - - template: linux/snap-build-linux.yml + - ${{ if eq(variables['VSCODE_BUILD_STAGE_MACOS'], true) }}: + - macOS + condition: succeededOrFailed() + pool: + vmImage: "Ubuntu-18.04" + jobs: + - job: SyncMooncake + displayName: Sync Mooncake + steps: + - template: sync-mooncake.yml - - job: LinuxArmhf - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ARMHF'], 'true')) - container: vscode-armhf - variables: - VSCODE_ARCH: armhf - NPM_ARCH: armv7l - steps: - - template: linux/product-build-linux.yml - - - job: LinuxSnapArmhf - dependsOn: - - LinuxArmhf - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ARMHF'], 'true')) - container: snapcraft - variables: - VSCODE_ARCH: armhf - steps: - - template: linux/snap-build-linux.yml - - - job: LinuxArm64 - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ARM64'], 'true')) - container: vscode-arm64 - variables: - VSCODE_ARCH: arm64 - NPM_ARCH: arm64 - steps: - - template: linux/product-build-linux.yml - - - job: LinuxSnapArm64 - dependsOn: - - LinuxArm64 - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ARM64'], 'true')) - container: snapcraft - variables: - VSCODE_ARCH: arm64 - steps: - - template: linux/snap-build-linux.yml - - - job: LinuxAlpine - condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX_ALPINE'], 'true')) - steps: - - template: linux/product-build-alpine.yml - - - job: LinuxWeb - condition: and(succeeded(), eq(variables['VSCODE_BUILD_WEB'], 'true')) - variables: - VSCODE_ARCH: x64 - steps: - - template: web/product-build-web.yml - - - stage: macOS - dependsOn: - - Compile - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) - pool: - vmImage: macOS-latest - jobs: - - job: macOS - condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS'], 'true')) - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - - - stage: macOSARM64 - dependsOn: - - Compile - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) - pool: - vmImage: macOS-latest - jobs: - - job: macOSARM64 - condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS_ARM64'], 'true')) - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: arm64 - steps: - - template: darwin/product-build-darwin.yml - - - stage: Mooncake - dependsOn: - - Windows - - Linux - - macOS - - macOSARM64 - condition: and(succeededOrFailed(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) - pool: - vmImage: "Ubuntu-16.04" - jobs: - - job: SyncMooncake - displayName: Sync Mooncake - steps: - - template: sync-mooncake.yml - - - stage: Publish - dependsOn: - - Windows - - Linux - - macOS - - macOSARM64 - condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), or(eq(variables['VSCODE_RELEASE'], 'true'), and(or(eq(variables['VSCODE_QUALITY'], 'insider'), eq(variables['VSCODE_QUALITY'], 'exploration')), eq(variables['Build.Reason'], 'Schedule')))) - pool: - vmImage: "Ubuntu-16.04" - jobs: - - job: BuildService - displayName: Build Service - steps: - - template: release.yml + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), or(eq(parameters.VSCODE_RELEASE, true), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true)))) }}: + - stage: Release + dependsOn: + - ${{ if eq(variables['VSCODE_BUILD_STAGE_WINDOWS'], true) }}: + - Windows + - ${{ if eq(variables['VSCODE_BUILD_STAGE_LINUX'], true) }}: + - Linux + - ${{ if eq(variables['VSCODE_BUILD_STAGE_MACOS'], true) }}: + - macOS + pool: + vmImage: "Ubuntu-18.04" + jobs: + - job: ReleaseBuild + displayName: Release Build + steps: + - template: release.yml diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index f606ad716d..215fc00952 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,36 +1,17 @@ steps: - - script: | - mkdir -p .build - echo -n $BUILD_SOURCEVERSION > .build/commit - echo -n $VSCODE_QUALITY > .build/quality - echo -n $ENABLE_TERRAPIN > .build/terrapin - displayName: Prepare compilation cache flag - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - dryRun: true - - task: NodeTool@0 inputs: - versionSpec: "12.14.1" - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: versionSpec: "1.x" - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" inputs: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - script: | set -e @@ -43,24 +24,22 @@ steps: git config user.email "vscode@microsoft.com" git config user.name "VSCode" displayName: Prepare tooling - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - script: | set -e - git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" - git fetch distro - git merge $(node -p "require('./package.json').distro") + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") displayName: Merge distro - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - script: | npx https://aka.ms/enablesecurefeed standAlone displayName: Switch to Terrapin packages timeoutInMinutes: 5 - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) + condition: and(succeeded(), eq(variables['ENABLE_TERRAPIN'], 'true')) - script: | + mkdir -p .build echo -n $(VSCODE_ARCH) > .build/arch + echo -n $ENABLE_TERRAPIN > .build/terrapin displayName: Prepare yarn cache flags - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 @@ -68,11 +47,22 @@ steps: keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" targetfolder: "**/node_modules, !**/node_modules/**/node_modules" vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - script: | set -e - export CHILD_CONCURRENCY="1" + export npm_config_arch=$(NPM_ARCH) + + if [ -z "$CC" ] || [ -z "$CXX" ]; then + export CC=$(which gcc-5) + export CXX=$(which g++-5) + fi + + if [ "$VSCODE_ARCH" == "x64" ]; then + export VSCODE_REMOTE_CC=$(which gcc-4.8) + export VSCODE_REMOTE_CXX=$(which g++-4.8) + export VSCODE_REMOTE_NODE_GYP=$(which node-gyp) + fi + for i in {1..3}; do # try 3 times, for Terrapin yarn --frozen-lockfile && break if [ $i -eq 3 ]; then @@ -81,50 +71,24 @@ steps: fi echo "Yarn failed $i, trying again..." done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 displayName: Install dependencies - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 inputs: keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" targetfolder: "**/node_modules, !**/node_modules/**/node_modules" vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - script: | - set -e - yarn postinstall - displayName: Run postinstall scripts - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), eq(variables['CacheRestored'], 'true')) - - # Mixin must run before optimize, because the CSS loader will - # inline small SVGs + # Mixin must run before optimize, because the CSS loader will inline small SVGs - script: | set -e node build/azure-pipelines/mixin displayName: Mix in quality - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - - - script: | - set -e - yarn gulp hygiene - yarn monaco-compile-check - yarn valid-layers-check - displayName: Run hygiene, monaco compile & valid layers checks - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set - - ./build/azure-pipelines/common/extract-telemetry.sh - displayName: Extract Telemetry - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - - - script: | - set -e - AZURE_WEBVIEW_STORAGE_ACCESS_KEY="$(vscode-webview-storage-key)" \ - ./build/azure-pipelines/common/publish-webview.sh - displayName: Publish Webview - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - script: | set -e @@ -134,14 +98,13 @@ steps: yarn gulp vscode-reh-linux-x64-min yarn gulp vscode-reh-web-linux-x64-min displayName: Compile - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - script: | set -e AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ node build/azure-pipelines/upload-sourcemaps displayName: Upload sourcemaps - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e @@ -149,13 +112,16 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ node build/azure-pipelines/common/createBuild.js $VERSION displayName: Create build - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 + # we gotta tarball everything in order to preserve file permissions + - script: | + set -e + tar -czf $(Build.ArtifactStagingDirectory)/compilation.tar.gz .build out-* + displayName: Compress compilation artifact + + - task: PublishPipelineArtifact@1 inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) + targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz + artifactName: Compilation + displayName: Publish compilation artifact diff --git a/build/azure-pipelines/publish-types/.gitignore b/build/azure-pipelines/publish-types/.gitignore deleted file mode 100644 index e94ecda764..0000000000 --- a/build/azure-pipelines/publish-types/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -*.js \ No newline at end of file diff --git a/build/azure-pipelines/publish-types/check-version.js b/build/azure-pipelines/publish-types/check-version.js new file mode 100644 index 0000000000..95563bf95e --- /dev/null +++ b/build/azure-pipelines/publish-types/check-version.js @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const cp = require("child_process"); +let tag = ''; +try { + tag = cp + .execSync('git describe --tags `git rev-list --tags --max-count=1`') + .toString() + .trim(); + if (!isValidTag(tag)) { + throw Error(`Invalid tag ${tag}`); + } +} +catch (err) { + console.error(err); + console.error('Failed to update types'); + process.exit(1); +} +function isValidTag(t) { + if (t.split('.').length !== 3) { + return false; + } + const [major, minor, bug] = t.split('.'); + // Only release for tags like 1.34.0 + if (bug !== '0') { + return false; + } + if (isNaN(parseInt(major, 10)) || isNaN(parseInt(minor, 10))) { + return false; + } + return true; +} diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index 5f1cf6c28a..0e3f4e4daa 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -9,7 +9,7 @@ pr: none steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/publish-types/update-types.js b/build/azure-pipelines/publish-types/update-types.js new file mode 100644 index 0000000000..b22dc60af5 --- /dev/null +++ b/build/azure-pipelines/publish-types/update-types.js @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const cp = require("child_process"); +const path = require("path"); +let tag = ''; +try { + tag = cp + .execSync('git describe --tags `git rev-list --tags --max-count=1`') + .toString() + .trim(); + const dtsUri = `https://raw.githubusercontent.com/microsoft/vscode/${tag}/src/vs/vscode.d.ts`; + const outPath = path.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); + cp.execSync(`curl ${dtsUri} --output ${outPath}`); + updateDTSFile(outPath, tag); + console.log(`Done updating vscode.d.ts at ${outPath}`); +} +catch (err) { + console.error(err); + console.error('Failed to update types'); + process.exit(1); +} +function updateDTSFile(outPath, tag) { + const oldContent = fs.readFileSync(outPath, 'utf-8'); + const newContent = getNewFileContent(oldContent, tag); + fs.writeFileSync(outPath, newContent); +} +function repeat(str, times) { + const result = new Array(times); + for (let i = 0; i < times; i++) { + result[i] = str; + } + return result.join(''); +} +function convertTabsToSpaces(str) { + return str.replace(/\t/gm, value => repeat(' ', value.length)); +} +function getNewFileContent(content, tag) { + const oldheader = [ + `/*---------------------------------------------------------------------------------------------`, + ` * Copyright (c) Microsoft Corporation. All rights reserved.`, + ` * Licensed under the Source EULA. See License.txt in the project root for license information.`, + ` *--------------------------------------------------------------------------------------------*/` + ].join('\n'); + return convertTabsToSpaces(getNewFileHeader(tag) + content.slice(oldheader.length)); +} +function getNewFileHeader(tag) { + const [major, minor] = tag.split('.'); + const shorttag = `${major}.${minor}`; + const header = [ + `// Type definitions for Visual Studio Code ${shorttag}`, + `// Project: https://github.com/microsoft/vscode`, + `// Definitions by: Visual Studio Code Team, Microsoft `, + `// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped`, + ``, + `/*---------------------------------------------------------------------------------------------`, + ` * Copyright (c) Microsoft Corporation. All rights reserved.`, + ` * Licensed under the Source EULA.`, + ` * See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.`, + ` *--------------------------------------------------------------------------------------------*/`, + ``, + `/**`, + ` * Type Definition for Visual Studio Code ${shorttag} Extension API`, + ` * See https://code.visualstudio.com/api for more information`, + ` */` + ].join('\n'); + return header; +} diff --git a/build/azure-pipelines/sql-product-build.yml b/build/azure-pipelines/sql-product-build.yml index 228f8aa7cc..8351692111 100644 --- a/build/azure-pipelines/sql-product-build.yml +++ b/build/azure-pipelines/sql-product-build.yml @@ -1,7 +1,7 @@ resources: containers: - container: linux-x64 - image: sqltoolscontainers.azurecr.io/linux-build-agent:2 + image: sqltoolscontainers.azurecr.io/linux-build-agent:3 endpoint: ContainerRegistry jobs: diff --git a/build/azure-pipelines/sync-mooncake.yml b/build/azure-pipelines/sync-mooncake.yml index 109709418f..280c9e6372 100644 --- a/build/azure-pipelines/sync-mooncake.yml +++ b/build/azure-pipelines/sync-mooncake.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 33282adbe4..05aa68fe12 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,28 +1,7 @@ steps: - - script: | - mkdir -p .build - echo -n $BUILD_SOURCEVERSION > .build/commit - echo -n $VSCODE_QUALITY > .build/quality - echo -n $ENABLE_TERRAPIN > .build/terrapin - displayName: Prepare compilation cache flag - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - - - script: | - set -e - exit 1 - displayName: Check RestoreCache - condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true')) - - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -34,6 +13,17 @@ steps: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - script: | + set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output + - script: | set -e cat << EOF > ~/.netrc @@ -48,30 +38,35 @@ steps: - script: | set -e - git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" - git fetch distro - git merge $(node -p "require('./package.json').distro") + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") displayName: Merge distro - script: | - npx https://aka.ms/enablesecurefeed standAlone - displayName: Switch to Terrapin packages - timeoutInMinutes: 5 - condition: and(succeeded(), eq(variables['ENABLE_TERRAPIN'], 'true')) + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js "web" $ENABLE_TERRAPIN > .build/yarnlockhash + displayName: Prepare yarn cache flags - - script: | - echo -n "web" > .build/arch - displayName: Prepare yarn cache flag - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + - task: Cache@2 inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" + key: 'nodeModules | $(Agent.OS) | .build/yarnlockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - script: | + set -e + tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + 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 - script: | set -e - export CHILD_CONCURRENCY="1" for i in {1..3}; do # try 3 times, for Terrapin yarn --frozen-lockfile && break if [ $i -eq 3 ]; then @@ -80,21 +75,19 @@ steps: fi echo "Yarn failed $i, trying again..." done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 displayName: Install dependencies - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | set -e - yarn postinstall - displayName: Run postinstall scripts - condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive - script: | set -e @@ -130,3 +123,8 @@ steps: VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ ./build/azure-pipelines/web/publish.sh displayName: Publish + + - publish: $(Agent.BuildDirectory)/vscode-web.tar.gz + artifact: vscode-web-standalone + displayName: Publish web archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/win32/ESRPClient/NuGet.config b/build/azure-pipelines/win32/ESRPClient/NuGet.config index e44c76a927..4ef337fa56 100644 --- a/build/azure-pipelines/win32/ESRPClient/NuGet.config +++ b/build/azure-pipelines/win32/ESRPClient/NuGet.config @@ -1,7 +1,10 @@ - - - - + + + + + + + diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 00e87931ca..e1e3fe0831 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 # {{SQL CARBON EDIT}} update version inputs: diff --git a/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 b/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 index f11f878c83..e9a1d5a8ae 100644 --- a/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 +++ b/build/azure-pipelines/win32/import-esrp-auth-cert.ps1 @@ -3,7 +3,7 @@ $ErrorActionPreference = "Stop" $CertBytes = [System.Convert]::FromBase64String($CertBase64) $CertCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection -$CertCollection.Import($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) +$CertCollection.Import($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) $CertStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") $CertStore.Open("ReadWrite") diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 1992ac1aff..035580035b 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -1,28 +1,7 @@ steps: - - powershell: | - mkdir .build -ea 0 - "$env:BUILD_SOURCEVERSION" | Out-File -Encoding ascii -NoNewLine .build\commit - "$env:VSCODE_QUALITY" | Out-File -Encoding ascii -NoNewLine .build\quality - "$env:ENABLE_TERRAPIN" | Out-File -Encoding ascii -NoNewLine .build\terrapin - displayName: Prepare compilation cache flags - - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: "build/.cachesalt, .build/commit, .build/quality, .build/terrapin" - targetfolder: ".build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min" - vstsFeed: "npm-vscode" - platformIndependent: true - alias: "Compilation" - - - powershell: | - $ErrorActionPreference = "Stop" - exit 1 - displayName: Check RestoreCache - condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true')) - - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -39,6 +18,18 @@ steps: azureSubscription: "vscode-builds-subscription" KeyVaultName: vscode + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { tar --force-local -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz } + displayName: Extract compilation output + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -51,26 +42,34 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" } - exec { git fetch distro } - exec { git merge $(node -p "require('./package.json').distro") } + exec { git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") } displayName: Merge distro - - script: | - npx https://aka.ms/enablesecurefeed standAlone - displayName: Switch to Terrapin packages - timeoutInMinutes: 5 - condition: and(succeeded(), eq(variables['ENABLE_TERRAPIN'], 'true')) - - powershell: | "$(VSCODE_ARCH)" | Out-File -Encoding ascii -NoNewLine .build\arch + "$env:ENABLE_TERRAPIN" | Out-File -Encoding ascii -NoNewLine .build\terrapin + node build/azure-pipelines/common/computeNodeModulesCacheKey.js > .build/yarnlockhash displayName: Prepare yarn cache flags - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + - task: Cache@2 inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" + key: 'nodeModules | $(Agent.OS) | .build/arch, .build/terrapin, .build/yarnlockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { 7z.exe x .build/node_modules_cache/cache.7z -aos } + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + 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 - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -79,22 +78,20 @@ steps: $env:npm_config_arch="$(VSCODE_ARCH)" $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile } } + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 displayName: Install dependencies - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - inputs: - keyfile: ".build/arch, .build/terrapin, build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock" - targetfolder: "**/node_modules, !**/node_modules/**/node_modules" - vstsFeed: "npm-vscode" - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { yarn postinstall } - displayName: Run postinstall scripts - condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + exec { node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt } + exec { mkdir -Force .build/node_modules_cache } + exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -107,11 +104,18 @@ steps: $ErrorActionPreference = "Stop" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } - exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-code-helper" } - exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)" displayName: Build + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-code-helper" } + exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } + displayName: Prepare Package + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -122,12 +126,21 @@ steps: displayName: Build Server condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" } + displayName: Download Electron and Playwright + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" exec { yarn electron $(VSCODE_ARCH) } exec { .\scripts\test.bat --build --tfs "Unit Tests" } displayName: Run unit tests (Electron) + timeoutInMinutes: 7 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | @@ -135,6 +148,14 @@ steps: $ErrorActionPreference = "Stop" exec { yarn test-browser --build --browser chromium --browser firefox --tfs "Browser Unit Tests" } displayName: Run unit tests (Browser) + timeoutInMinutes: 7 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn --cwd test/integration/browser compile } + displayName: Compile integration tests condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | @@ -148,6 +169,7 @@ steps: $AppNameShort = $AppProductJson.nameShort exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } displayName: Run integration tests (Electron) + timeoutInMinutes: 10 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | @@ -155,6 +177,7 @@ steps: $ErrorActionPreference = "Stop" exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser firefox } displayName: Run integration tests (Browser) + timeoutInMinutes: 7 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - powershell: | @@ -165,6 +188,7 @@ steps: $AppNameShort = $AppProductJson.nameShort exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-remote-integration.bat } displayName: Run remote integration tests (Electron) + timeoutInMinutes: 7 condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - task: PublishPipelineArtifact@0 @@ -180,7 +204,7 @@ steps: inputs: testResultsFiles: "*-results.xml" searchFolder: "$(Build.ArtifactStagingDirectory)/test-results" - condition: and(succeededOrFailed(), ne(variables['VSCODE_ARCH'], 'arm64')) + condition: and(succeededOrFailed(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: @@ -236,6 +260,7 @@ steps: } ] SessionTimeout: 120 + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: NuGetCommand@2 displayName: Install ESRPClient.exe @@ -245,11 +270,13 @@ steps: nugetConfigPath: 'build\azure-pipelines\win32\ESRPClient\NuGet.config' externalFeedCredentials: "ESRP Nuget" restoreDirectory: packages + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: ESRPImportCertTask@1 displayName: Import ESRP Request Signing Certificate inputs: ESRP: "ESRP CodeSign" + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: PowerShell@2 inputs: @@ -257,6 +284,7 @@ steps: filePath: .\build\azure-pipelines\win32\import-esrp-auth-cert.ps1 arguments: "$(ESRP-SSL-AADAuth)" displayName: Import ESRP Auth Certificate + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - powershell: | . build/azure-pipelines/win32/exec.ps1 @@ -266,6 +294,32 @@ steps: $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" .\build\azure-pipelines\win32\publish.ps1 displayName: Publish + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\archive\VSCode-win32-$(VSCODE_ARCH).zip + artifact: vscode-win32-$(VSCODE_ARCH) + displayName: Publish archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe + artifact: vscode-win32-$(VSCODE_ARCH)-setup + displayName: Publish system setup + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(System.DefaultWorkingDirectory)\.build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe + artifact: vscode-win32-$(VSCODE_ARCH)-user-setup + displayName: Publish user setup + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + + - publish: $(System.DefaultWorkingDirectory)\.build\vscode-server-win32-$(VSCODE_ARCH).zip + artifact: vscode-server-win32-$(VSCODE_ARCH) + displayName: Publish server archive + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + + - publish: $(System.DefaultWorkingDirectory)\.build\vscode-server-win32-$(VSCODE_ARCH)-web.zip + 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" diff --git a/build/builtin/browser-main.js b/build/builtin/browser-main.js index db335a363e..3fe8f98267 100644 --- a/build/builtin/browser-main.js +++ b/build/builtin/browser-main.js @@ -6,8 +6,7 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -const { remote } = require('electron'); -const dialog = remote.dialog; +const { ipcRenderer } = require('electron'); const builtInExtensionsPath = path.join(__dirname, '..', '..', 'product.json'); const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); @@ -84,17 +83,13 @@ function render(el, state) { } const localInput = renderOption(form, `local-${ext.name}`, 'Local', 'local', !!local); - localInput.onchange = function () { - const result = dialog.showOpenDialog(remote.getCurrentWindow(), { - title: 'Choose Folder', - properties: ['openDirectory'] - }); + localInput.onchange = async function () { + const result = await ipcRenderer.invoke('pickdir'); - if (result && result.length >= 1) { - control[ext.name] = result[0]; + if (result) { + control[ext.name] = result; + setState({ builtin, control }); } - - setState({ builtin, control }); }; if (local) { diff --git a/build/builtin/main.js b/build/builtin/main.js index 4b241776d5..2fa5a7ce82 100644 --- a/build/builtin/main.js +++ b/build/builtin/main.js @@ -3,12 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const { app, BrowserWindow } = require('electron'); +const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const url = require('url'); const path = require('path'); let window = null; +ipcMain.handle('pickdir', async () => { + const result = await dialog.showOpenDialog(window, { + title: 'Choose Folder', + properties: ['openDirectory'] + }); + + if (result.canceled || result.filePaths.length < 1) { + return undefined; + } + + return result.filePaths[0]; +}); + app.once('ready', () => { window = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, webviewTag: true, enableWebSQL: false, nativeWindowOpen: true } }); window.setMenuBarVisibility(false); diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js new file mode 100644 index 0000000000..c2cd24fe04 --- /dev/null +++ b/build/darwin/create-universal-app.js @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const vscode_universal_1 = require("vscode-universal"); +const fs = require("fs-extra"); +const path = require("path"); +const plist = require("plist"); +const product = require("../../product.json"); +async function main() { + const buildDir = process.env['AGENT_BUILDDIRECTORY']; + const arch = process.env['VSCODE_ARCH']; + if (!buildDir) { + throw new Error('$AGENT_BUILDDIRECTORY not set'); + } + const appName = product.nameLong + '.app'; + const x64AppPath = path.join(buildDir, 'vscode-x64', appName); + const arm64AppPath = path.join(buildDir, 'vscode-arm64', appName); + const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); + const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const infoPlistPath = path.resolve(outAppPath, 'Contents', 'Info.plist'); + await vscode_universal_1.makeUniversalApp({ + x64AppPath, + arm64AppPath, + x64AsarPath, + arm64AsarPath, + filesToSkip: [ + 'product.json', + 'Credits.rtf', + 'CodeResources', + 'fsevents.node', + '.npmrc' + ], + outAppPath, + force: true + }); + let productJson = await fs.readJson(productJsonPath); + Object.assign(productJson, { + darwinUniversalAssetId: 'darwin-universal' + }); + await fs.writeJson(productJsonPath, productJson); + let infoPlistString = await fs.readFile(infoPlistPath, 'utf8'); + let infoPlistJson = plist.parse(infoPlistString); + Object.assign(infoPlistJson, { + LSRequiresNativeExecution: true + }); + await fs.writeFile(infoPlistPath, plist.build(infoPlistJson), 'utf8'); +} +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts new file mode 100644 index 0000000000..8ae2c2b116 --- /dev/null +++ b/build/darwin/create-universal-app.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { makeUniversalApp } from 'vscode-universal'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as plist from 'plist'; +import * as product from '../../product.json'; + +async function main() { + const buildDir = process.env['AGENT_BUILDDIRECTORY']; + const arch = process.env['VSCODE_ARCH']; + + if (!buildDir) { + throw new Error('$AGENT_BUILDDIRECTORY not set'); + } + + const appName = product.nameLong + '.app'; + const x64AppPath = path.join(buildDir, 'vscode-x64', appName); + const arm64AppPath = path.join(buildDir, 'vscode-arm64', appName); + const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); + const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const infoPlistPath = path.resolve(outAppPath, 'Contents', 'Info.plist'); + + await makeUniversalApp({ + x64AppPath, + arm64AppPath, + x64AsarPath, + arm64AsarPath, + filesToSkip: [ + 'product.json', + 'Credits.rtf', + 'CodeResources', + 'fsevents.node', + '.npmrc' + ], + outAppPath, + force: true + }); + + let productJson = await fs.readJson(productJsonPath); + Object.assign(productJson, { + darwinUniversalAssetId: 'darwin-universal' + }); + await fs.writeJson(productJsonPath, productJson); + + let infoPlistString = await fs.readFile(infoPlistPath, 'utf8'); + let infoPlistJson = plist.parse(infoPlistString); + Object.assign(infoPlistJson, { + LSRequiresNativeExecution: true + }); + await fs.writeFile(infoPlistPath, plist.build(infoPlistJson), 'utf8'); +} + +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/darwin/sign.js b/build/darwin/sign.js index cd8376f775..fe33fed795 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -24,7 +24,6 @@ async function main() { const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; - const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; const defaultOpts = { app: path.join(appRoot, appName), @@ -43,14 +42,11 @@ async function main() { // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath) => { return filePath.includes(gpuHelperAppName) || - filePath.includes(pluginHelperAppName) || filePath.includes(rendererHelperAppName); } }); const gpuHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, gpuHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist') }); - const pluginHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, pluginHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist') }); const rendererHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, rendererHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist') }); await codesign.signAsync(gpuHelperOpts); - await codesign.signAsync(pluginHelperOpts); await codesign.signAsync(rendererHelperOpts); await codesign.signAsync(appOpts); } diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index b5c5eb7150..cc7a44fdf3 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -29,7 +29,6 @@ async function main(): Promise { const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; - const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; const defaultOpts: codesign.SignOptions = { @@ -51,7 +50,6 @@ async function main(): Promise { // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath: string) => { return filePath.includes(gpuHelperAppName) || - filePath.includes(pluginHelperAppName) || filePath.includes(rendererHelperAppName); } }; @@ -63,13 +61,6 @@ async function main(): Promise { 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), }; - const pluginHelperOpts: codesign.SignOptions = { - ...defaultOpts, - app: path.join(appFrameworkPath, pluginHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), - }; - const rendererHelperOpts: codesign.SignOptions = { ...defaultOpts, app: path.join(appFrameworkPath, rendererHelperAppName), @@ -78,7 +69,6 @@ async function main(): Promise { }; await codesign.signAsync(gpuHelperOpts); - await codesign.signAsync(pluginHelperOpts); await codesign.signAsync(rendererHelperOpts); await codesign.signAsync(appOpts as any); } diff --git a/build/eslint.js b/build/eslint.js new file mode 100644 index 0000000000..e76f7c9af4 --- /dev/null +++ b/build/eslint.js @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const es = require('event-stream'); +const vfs = require('vinyl-fs'); +const { jsHygieneFilter, tsHygieneFilter } = require('./filters'); + +function eslint() { + const gulpeslint = require('gulp-eslint'); + return vfs + .src([...jsHygieneFilter, ...tsHygieneFilter], { base: '.', follow: true, allowEmpty: true }) + .pipe( + gulpeslint({ + configFile: '.eslintrc.json', + rulePaths: ['./build/lib/eslint'], + }) + ) + .pipe(gulpeslint.formatEach('compact')) + .pipe( + gulpeslint.results((results) => { + if (results.warningCount > 0 || results.errorCount > 0) { + throw new Error('eslint failed with warnings and/or errors'); + } + }) + ).pipe(es.through(function () { /* noop, important for the stream to end */ })); +} + +if (require.main === module) { + eslint().on('error', (err) => { + console.error(); + console.error(err); + process.exit(1); + }); +} diff --git a/build/filters.js b/build/filters.js new file mode 100644 index 0000000000..32b8eaeeb7 --- /dev/null +++ b/build/filters.js @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Hygiene works by creating cascading subsets of all our files and + * passing them through a sequence of checks. Here are the current subsets, + * named according to the checks performed on them. Each subset contains + * the following one, as described in mathematical notation: + * + * all ⊃ eol ⊇ indentation ⊃ copyright ⊃ typescript + */ + +module.exports.all = [ + '*', + 'build/**/*', + 'extensions/**/*', + 'scripts/**/*', + 'src/**/*', + 'test/**/*', + '!out*/**', + '!test/**/out/**', + '!**/node_modules/**', +]; + +module.exports.indentationFilter = [ + '**', + + // except specific files + '!**/ThirdPartyNotices.txt', + '!**/LICENSE.{txt,rtf}', + '!LICENSES.chromium.html', + '!**/LICENSE', + '!src/vs/nls.js', + '!src/vs/nls.build.js', + '!src/vs/css.js', + '!src/vs/css.build.js', + '!src/vs/loader.js', + '!src/vs/base/common/insane/insane.js', + '!src/vs/base/common/marked/marked.js', + '!src/vs/base/common/semver/semver.js', + '!src/vs/base/node/terminateProcess.sh', + '!src/vs/base/node/cpuUsage.sh', + '!test/unit/assert.js', + '!resources/linux/snap/electron-launch', + '!build/ext.js', + + // except specific folders + '!test/automation/out/**', + '!test/monaco/out/**', + '!test/smoke/out/**', + '!extensions/typescript-language-features/test-workspace/**', + '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**', + '!build/monaco/**', + '!build/win32/**', + + // except multiple specific files + '!**/package.json', + '!**/yarn.lock', + '!**/yarn-error.log', + + // except multiple specific folders + '!**/codicon/**', + '!**/fixtures/**', + '!**/lib/**', + '!extensions/**/dist/**', + '!extensions/**/out/**', + '!extensions/**/snippets/**', + '!extensions/**/syntaxes/**', + '!extensions/**/themes/**', + '!extensions/**/colorize-fixtures/**', + + // except specific file types + '!src/vs/*/**/*.d.ts', + '!src/typings/**/*.d.ts', + '!extensions/**/*.d.ts', + '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns,plist}', + '!build/{lib,download,darwin}/**/*.js', + '!build/**/*.sh', + '!build/azure-pipelines/**/*.js', + '!build/azure-pipelines/**/*.config', + '!**/Dockerfile', + '!**/Dockerfile.*', + '!**/*.Dockerfile', + '!**/*.dockerfile', + '!extensions/markdown-language-features/media/*.js', + '!extensions/simple-browser/media/*.js', +]; + +module.exports.copyrightFilter = [ + '**', + '!**/*.desktop', + '!**/*.json', + '!**/*.html', + '!**/*.template', + '!**/*.md', + '!**/*.bat', + '!**/*.cmd', + '!**/*.ico', + '!**/*.icns', + '!**/*.xml', + '!**/*.sh', + '!**/*.txt', + '!**/*.xpm', + '!**/*.opts', + '!**/*.disabled', + '!**/*.code-workspace', + '!**/*.js.map', + '!build/**/*.init', + '!resources/linux/snap/snapcraft.yaml', + '!resources/win32/bin/code.js', + '!resources/web/code-web.js', + '!resources/completions/**', + '!extensions/configuration-editing/build/inline-allOf.ts', + '!extensions/markdown-language-features/media/highlight.css', + '!extensions/html-language-features/server/src/modes/typescript/*', + '!extensions/*/server/bin/*', + '!src/vs/editor/test/node/classification/typescript-test.ts', +]; + +module.exports.jsHygieneFilter = [ + 'src/**/*.js', + 'build/gulpfile.*.js', + '!src/vs/loader.js', + '!src/vs/css.js', + '!src/vs/nls.js', + '!src/vs/css.build.js', + '!src/vs/nls.build.js', + '!src/**/insane.js', + '!src/**/marked.js', + '!src/**/semver.js', + '!**/test/**', +]; + +module.exports.tsHygieneFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + 'extensions/**/*.ts', + '!src/vs/*/**/*.d.ts', + '!src/typings/**/*.d.ts', + '!extensions/**/*.d.ts', + '!**/fixtures/**', + '!**/typings/**', + '!**/node_modules/**', + '!extensions/**/colorize-fixtures/**', + '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**', + '!extensions/**/*.test.ts', + '!extensions/html-language-features/server/lib/jquery.d.ts', +]; diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js index 6bd412b8e4..429a2748a9 100644 --- a/build/gulpfile.compile.js +++ b/build/gulpfile.compile.js @@ -11,6 +11,11 @@ const task = require('./lib/task'); const compilation = require('./lib/compilation'); // Full compile, including nls and inline sources in sourcemaps, for build -const compileBuildTask = task.define('compile-build', task.series(util.rimraf('out-build'), compilation.compileTask('src', 'out-build', true))); +const compileBuildTask = task.define('compile-build', + task.series( + util.rimraf('out-build'), + compilation.compileTask('src', 'out-build', true) + ) +); gulp.task(compileBuildTask); exports.compileBuildTask = compileBuildTask; diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 6cf5e1c445..0e56c448f0 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -16,8 +16,6 @@ const cp = require('child_process'); const compilation = require('./lib/compilation'); const monacoapi = require('./monaco/api'); const fs = require('fs'); -const webpack = require('webpack'); -const webpackGulp = require('webpack-stream'); let root = path.dirname(__dirname); let sha1 = util.getVersion(root); @@ -369,6 +367,9 @@ gulp.task('editor-distro', ); const bundleEditorESMTask = task.define('editor-esm-bundle-webpack', () => { + const webpack = require('webpack'); + const webpackGulp = require('webpack-stream'); + const result = es.through(); const webpackConfigPath = path.join(root, 'build/monaco/monaco.webpack.config.js'); diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 713c2a4299..46b7c06414 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -9,17 +9,13 @@ require('events').EventEmitter.defaultMaxListeners = 100; const gulp = require('gulp'); const path = require('path'); const nodeUtil = require('util'); -const tsb = require('gulp-tsb'); const es = require('event-stream'); const filter = require('gulp-filter'); -const webpack = require('webpack'); const util = require('./lib/util'); const task = require('./lib/task'); const watcher = require('./lib/watch'); const createReporter = require('./lib/reporter').createReporter; const glob = require('glob'); -const sourcemaps = require('gulp-sourcemaps'); -const nlsDev = require('vscode-nls-dev'); const root = path.dirname(__dirname); const commit = util.getVersion(root); const plumber = require('gulp-plumber'); @@ -70,6 +66,10 @@ const tasks = compilations.map(function (tsconfigFile) { } function createPipeline(build, emitError) { + const nlsDev = require('vscode-nls-dev'); + const tsb = require('gulp-tsb'); + const sourcemaps = require('gulp-sourcemaps'); + const reporter = createReporter('extensions'); overrideOptions.inlineSources = Boolean(build); @@ -176,6 +176,8 @@ const compileExtensionsBuildTask = task.define('compile-extensions-build', task. )); gulp.task(compileExtensionsBuildTask); +gulp.task(task.define('extensions-ci', task.series(compileExtensionsBuildTask))); + exports.compileExtensionsBuildTask = compileExtensionsBuildTask; const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false)); @@ -187,6 +189,7 @@ gulp.task(watchWebExtensionsTask); exports.watchWebExtensionsTask = watchWebExtensionsTask; async function buildWebExtensions(isWatch) { + const webpack = require('webpack'); const webpackConfigLocations = await nodeUtil.promisify(glob)( path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), diff --git a/build/gulpfile.js b/build/gulpfile.js new file mode 100644 index 0000000000..a8427eb47d --- /dev/null +++ b/build/gulpfile.js @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +// Increase max listeners for event emitters +require('events').EventEmitter.defaultMaxListeners = 100; + +const gulp = require('gulp'); +const util = require('./lib/util'); +const task = require('./lib/task'); +const compilation = require('./lib/compilation'); +const { monacoTypecheckTask/* , monacoTypecheckWatchTask */ } = require('./gulpfile.editor'); +const { compileExtensionsTask, watchExtensionsTask } = require('./gulpfile.extensions'); + +// Fast compile for development time +const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.compileTask('src', 'out', false))); +gulp.task(compileClientTask); + +const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), compilation.watchTask('out', false))); +gulp.task(watchClientTask); + +// All +const compileTask = task.define('compile', task.parallel(monacoTypecheckTask, compileClientTask, compileExtensionsTask)); +gulp.task(compileTask); + +gulp.task(task.define('watch', task.parallel(/* monacoTypecheckWatchTask, */ watchClientTask, watchExtensionsTask))); + +// Default +gulp.task('default', compileTask); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); +}); + +// Load all the gulpfiles only if running tasks other than the editor tasks +require('glob').sync('gulpfile.*.js', { cwd: __dirname }) + .forEach(f => require(`./${f}`)); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 2679341f73..bffaf8ecbf 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -14,10 +14,8 @@ const task = require('./lib/task'); const vfs = require('vinyl-fs'); const flatmap = require('gulp-flatmap'); const gunzip = require('gulp-gunzip'); -const untar = require('gulp-untar'); const File = require('vinyl'); const fs = require('fs'); -const remote = require('gulp-remote-retry-src'); const rename = require('gulp-rename'); const filter = require('gulp-filter'); const cp = require('child_process'); @@ -78,13 +76,17 @@ BUILD_TARGETS.forEach(({ platform, arch }) => { })); }); -const defaultNodeTask = gulp.task(`node-${process.platform}-${process.arch}`); +const arch = process.platform === 'darwin' ? 'x64' : process.arch; +const defaultNodeTask = gulp.task(`node-${process.platform}-${arch}`); if (defaultNodeTask) { gulp.task(task.define('node', defaultNodeTask)); } function nodejs(platform, arch) { + const remote = require('gulp-remote-retry-src'); + const untar = require('gulp-untar'); + if (arch === 'ia32') { arch = 'x86'; } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 9997210084..37fa89a762 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -11,13 +11,10 @@ const os = require('os'); const cp = require('child_process'); const path = require('path'); const es = require('event-stream'); -const azure = require('gulp-azure-storage'); -const electron = require('gulp-atom-electron'); const vfs = require('vinyl-fs'); const rename = require('gulp-rename'); const replace = require('gulp-replace'); const filter = require('gulp-filter'); -const json = require('gulp-json-editor'); const _ = require('underscore'); const util = require('./lib/util'); const task = require('./lib/task'); @@ -29,14 +26,12 @@ const packageJson = require('../package.json'); const product = require('../product.json'); const crypto = require('crypto'); const i18n = require('./lib/i18n'); -const deps = require('./dependencies'); +const { getProductionDependencies } = require('./lib/dependencies'); const { config } = require('./lib/electron'); const createAsar = require('./lib/asar').createAsar; const { compileBuildTask } = require('./gulpfile.compile'); const { compileExtensionsBuildTask } = require('./gulpfile.extensions'); -const productionDependencies = deps.getProductionDependencies(path.dirname(__dirname)); - // Build const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.desktop.main'), @@ -57,7 +52,7 @@ const vscodeResources = [ 'out-build/bootstrap-node.js', 'out-build/bootstrap-window.js', 'out-build/paths.js', - 'out-build/vs/**/*.{svg,png,html}', + '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', @@ -97,7 +92,6 @@ const vscodeResources = [ 'out-build/sql/workbench/parts/notebook/media/**/*.svg', 'out-build/sql/setup.js', // {{SQL CARBON EDIT}} end 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js', - 'out-build/vs/code/electron-sandbox/proxy/auth.js', '!**/test/**' ]; @@ -122,6 +116,16 @@ const minifyVSCodeTask = task.define('minify-vscode', task.series( )); gulp.task(minifyVSCodeTask); +const core = task.define('core-ci', task.series( + gulp.task('compile-build'), + task.parallel( + gulp.task('minify-vscode'), + gulp.task('minify-vscode-reh'), + gulp.task('minify-vscode-reh-web'), + ) +)); +gulp.task(core); + /** * Compute checksums for some files. * @@ -163,6 +167,9 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op platform = platform || process.platform; return () => { + const electron = require('gulp-atom-electron'); + const json = require('gulp-json-editor'); + const out = sourceFolderName; const checksums = computeChecksums(out, [ @@ -221,8 +228,9 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const telemetry = gulp.src('.build/telemetry/**', { base: '.build/telemetry', dot: true }); - const jsFilter = util.filter(data => !data.isDirectory() &&/\.js$/.test(data.path)); + const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); const root = path.resolve(path.join(__dirname, '..')); + const productionDependencies = getProductionDependencies(root); const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(root, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`])); // {{SQL CARBON EDIT}} - fix runtime module load break @@ -357,7 +365,7 @@ BUILD_TARGETS.forEach(buildTarget => { const arch = buildTarget.arch; const opts = buildTarget.opts; - ['', 'min'].forEach(minified => { + const [vscode, vscodeMin] = ['', 'min'].map(minified => { const sourceFolderName = `out-vscode${dashed(minified)}`; const destinationFolderName = `azuredatastudio${dashed(platform)}${dashed(arch)}`; @@ -374,7 +382,14 @@ BUILD_TARGETS.forEach(buildTarget => { vscodeTaskCI )); gulp.task(vscodeTask); + + return vscodeTask; }); + + if (process.platform === platform && process.arch === arch) { + gulp.task(task.define('vscode', task.series(vscode))); + gulp.task(task.define('vscode-min', task.series(vscodeMin))); + } }); // Transifex Localizations @@ -516,6 +531,8 @@ gulp.task(task.define( task.series( generateVSCodeConfigurationTask, () => { + const azure = require('gulp-azure-storage'); + if (!shouldSetupSettingsSearch()) { const branch = process.env.BUILD_SOURCEBRANCH; console.log(`Only runs on master and release branches, not ${branch}`); diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index ac8af3845d..1f21d32495 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -96,9 +96,6 @@ function prepareDebPackage(arch) { const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@ARCHITECTURE@@', debArch)) - .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) - .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(rename('DEBIAN/postinst')); const all = es.merge(control, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); @@ -240,7 +237,8 @@ function prepareSnapPackage(arch) { const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@VERSION@@', commit.substr(0, 8))) - .pipe(replace('@@ARCHITECTURE@@', arch)) + // Possible run-on values https://snapcraft.io/docs/architectures + .pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch)) .pipe(rename('snap/snapcraft.yaml')); const electronLaunch = gulp.src('resources/linux/snap/electron-launch', { base: '.' }) diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 6afd8b1394..7495f0595a 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -1,133 +1,120 @@ +"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getBuiltInExtensions = void 0; +const fs = require("fs"); +const path = require("path"); +const os = require("os"); +const rimraf = require("rimraf"); +const es = require("event-stream"); +const rename = require("gulp-rename"); +const vfs = require("vinyl-fs"); +const ext = require("./extensions"); +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); const mkdirp = require('mkdirp'); -const rimraf = require('rimraf'); -const es = require('event-stream'); -const rename = require('gulp-rename'); -const vfs = require('vinyl-fs'); -const ext = require('./extensions'); -const fancyLog = require('fancy-log'); -const ansiColors = require('ansi-colors'); - const root = path.dirname(path.dirname(__dirname)); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions; const webBuiltInExtensions = productjson.webBuiltInExtensions; const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; - -function log() { - if (ENABLE_LOGGING) { - fancyLog.apply(this, arguments); - } +function log(...messages) { + if (ENABLE_LOGGING) { + fancyLog(...messages); + } } - function getExtensionPath(extension) { - return path.join(root, '.build', 'builtInExtensions', extension.name); + return path.join(root, '.build', 'builtInExtensions', extension.name); } - function isUpToDate(extension) { - const packagePath = path.join(getExtensionPath(extension), 'package.json'); - - if (!fs.existsSync(packagePath)) { - return false; - } - - const packageContents = fs.readFileSync(packagePath, { encoding: 'utf8' }); - - try { - const diskVersion = JSON.parse(packageContents).version; - return (diskVersion === extension.version); - } catch (err) { - return false; - } + const packagePath = path.join(getExtensionPath(extension), 'package.json'); + if (!fs.existsSync(packagePath)) { + return false; + } + const packageContents = fs.readFileSync(packagePath, { encoding: 'utf8' }); + try { + const diskVersion = JSON.parse(packageContents).version; + return (diskVersion === extension.version); + } + catch (err) { + return false; + } } - function syncMarketplaceExtension(extension) { - if (isUpToDate(extension)) { - log(ansiColors.blue('[marketplace]'), `${extension.name}@${extension.version}`, ansiColors.green('✔︎')); - return es.readArray([]); - } - - rimraf.sync(getExtensionPath(extension)); - - return ext.fromMarketplace(extension.name, extension.version, extension.metadata) - .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)) - .pipe(vfs.dest('.build/builtInExtensions')) - .on('end', () => log(ansiColors.blue('[marketplace]'), extension.name, ansiColors.green('✔︎'))); + if (isUpToDate(extension)) { + log(ansiColors.blue('[marketplace]'), `${extension.name}@${extension.version}`, ansiColors.green('✔︎')); + return es.readArray([]); + } + rimraf.sync(getExtensionPath(extension)); + return ext.fromMarketplace(extension.name, extension.version, extension.metadata) + .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)) + .pipe(vfs.dest('.build/builtInExtensions')) + .on('end', () => log(ansiColors.blue('[marketplace]'), extension.name, ansiColors.green('✔︎'))); } - function syncExtension(extension, controlState) { - switch (controlState) { - case 'disabled': - log(ansiColors.blue('[disabled]'), ansiColors.gray(extension.name)); - return es.readArray([]); - - case 'marketplace': - return syncMarketplaceExtension(extension); - - default: - if (!fs.existsSync(controlState)) { - log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); - return es.readArray([]); - - } else if (!fs.existsSync(path.join(controlState, 'package.json'))) { - log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); - return es.readArray([]); - } - - log(ansiColors.blue('[local]'), `${extension.name}: ${ansiColors.cyan(controlState)}`, ansiColors.green('✔︎')); - return es.readArray([]); - } + if (extension.platforms) { + const platforms = new Set(extension.platforms); + if (!platforms.has(process.platform)) { + log(ansiColors.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansiColors.green('✔︎')); + return es.readArray([]); + } + } + switch (controlState) { + case 'disabled': + log(ansiColors.blue('[disabled]'), ansiColors.gray(extension.name)); + return es.readArray([]); + case 'marketplace': + return syncMarketplaceExtension(extension); + default: + if (!fs.existsSync(controlState)) { + log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); + return es.readArray([]); + } + else if (!fs.existsSync(path.join(controlState, 'package.json'))) { + log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); + return es.readArray([]); + } + log(ansiColors.blue('[local]'), `${extension.name}: ${ansiColors.cyan(controlState)}`, ansiColors.green('✔︎')); + return es.readArray([]); + } } - function readControlFile() { - try { - return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); - } catch (err) { - return {}; - } + try { + return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); + } + catch (err) { + return {}; + } } - function writeControlFile(control) { - mkdirp.sync(path.dirname(controlFilePath)); - fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); + mkdirp.sync(path.dirname(controlFilePath)); + fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } - -exports.getBuiltInExtensions = function getBuiltInExtensions() { - log('Syncronizing built-in extensions...'); - log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); - - const control = readControlFile(); - const streams = []; - - for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { - let controlState = control[extension.name] || 'marketplace'; - control[extension.name] = controlState; - - streams.push(syncExtension(extension, controlState)); - } - - writeControlFile(control); - - return new Promise((resolve, reject) => { - es.merge(streams) - .on('error', reject) - .on('end', resolve); - }); -}; - +function getBuiltInExtensions() { + log('Syncronizing built-in extensions...'); + log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); + const control = readControlFile(); + const streams = []; + for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { + let controlState = control[extension.name] || 'marketplace'; + control[extension.name] = controlState; + streams.push(syncExtension(extension, controlState)); + } + writeControlFile(control); + return new Promise((resolve, reject) => { + es.merge(streams) + .on('error', reject) + .on('end', resolve); + }); +} +exports.getBuiltInExtensions = getBuiltInExtensions; if (require.main === module) { - exports.getBuiltInExtensions().then(() => process.exit(0)).catch(err => { - console.error(err); - process.exit(1); - }); + getBuiltInExtensions().then(() => process.exit(0)).catch(err => { + console.error(err); + process.exit(1); + }); } diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts new file mode 100644 index 0000000000..a9ae6d89a3 --- /dev/null +++ b/build/lib/builtInExtensions.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as rimraf from 'rimraf'; +import * as es from 'event-stream'; +import * as rename from 'gulp-rename'; +import * as vfs from 'vinyl-fs'; +import * as ext from './extensions'; +import * as fancyLog from 'fancy-log'; +import * as ansiColors from 'ansi-colors'; +import { Stream } from 'stream'; + +const mkdirp = require('mkdirp'); + +interface IExtensionDefinition { + name: string; + version: string; + repo: string; + platforms?: string[]; + metadata: { + id: string; + publisherId: { + publisherId: string; + publisherName: string; + displayName: string; + flags: string; + }; + publisherDisplayName: string; + } +} + +const root = path.dirname(path.dirname(__dirname)); +const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const builtInExtensions = productjson.builtInExtensions; +const webBuiltInExtensions = productjson.webBuiltInExtensions; +const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); +const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; + +function log(...messages: string[]): void { + if (ENABLE_LOGGING) { + fancyLog(...messages); + } +} + +function getExtensionPath(extension: IExtensionDefinition): string { + return path.join(root, '.build', 'builtInExtensions', extension.name); +} + +function isUpToDate(extension: IExtensionDefinition): boolean { + const packagePath = path.join(getExtensionPath(extension), 'package.json'); + + if (!fs.existsSync(packagePath)) { + return false; + } + + const packageContents = fs.readFileSync(packagePath, { encoding: 'utf8' }); + + try { + const diskVersion = JSON.parse(packageContents).version; + return (diskVersion === extension.version); + } catch (err) { + return false; + } +} + +function syncMarketplaceExtension(extension: IExtensionDefinition): Stream { + if (isUpToDate(extension)) { + log(ansiColors.blue('[marketplace]'), `${extension.name}@${extension.version}`, ansiColors.green('✔︎')); + return es.readArray([]); + } + + rimraf.sync(getExtensionPath(extension)); + + return ext.fromMarketplace(extension.name, extension.version, extension.metadata) + .pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`)) + .pipe(vfs.dest('.build/builtInExtensions')) + .on('end', () => log(ansiColors.blue('[marketplace]'), extension.name, ansiColors.green('✔︎'))); +} + +function syncExtension(extension: IExtensionDefinition, controlState: 'disabled' | 'marketplace'): Stream { + if (extension.platforms) { + const platforms = new Set(extension.platforms); + + if (!platforms.has(process.platform)) { + log(ansiColors.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansiColors.green('✔︎')); + return es.readArray([]); + } + } + + switch (controlState) { + case 'disabled': + log(ansiColors.blue('[disabled]'), ansiColors.gray(extension.name)); + return es.readArray([]); + + case 'marketplace': + return syncMarketplaceExtension(extension); + + default: + if (!fs.existsSync(controlState)) { + log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); + return es.readArray([]); + + } else if (!fs.existsSync(path.join(controlState, 'package.json'))) { + log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); + return es.readArray([]); + } + + log(ansiColors.blue('[local]'), `${extension.name}: ${ansiColors.cyan(controlState)}`, ansiColors.green('✔︎')); + return es.readArray([]); + } +} + +interface IControlFile { + [name: string]: 'disabled' | 'marketplace'; +} + +function readControlFile(): IControlFile { + try { + return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); + } catch (err) { + return {}; + } +} + +function writeControlFile(control: IControlFile): void { + mkdirp.sync(path.dirname(controlFilePath)); + fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); +} + +export function getBuiltInExtensions(): Promise { + log('Syncronizing built-in extensions...'); + log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); + + const control = readControlFile(); + const streams: Stream[] = []; + + for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { + let controlState = control[extension.name] || 'marketplace'; + control[extension.name] = controlState; + + streams.push(syncExtension(extension, controlState)); + } + + writeControlFile(control); + + return new Promise((resolve, reject) => { + es.merge(streams) + .on('error', reject) + .on('end', resolve); + }); +} + +if (require.main === module) { + getBuiltInExtensions().then(() => process.exit(0)).catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/lib/compilation.js b/build/lib/compilation.js index f8b50731c2..b5ff444b22 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -8,9 +8,6 @@ exports.watchTask = exports.compileTask = void 0; const es = require("event-stream"); const fs = require("fs"); const gulp = require("gulp"); -const bom = require("gulp-bom"); -const sourcemaps = require("gulp-sourcemaps"); -const tsb = require("gulp-tsb"); const path = require("path"); const monacodts = require("../monaco/api"); const nls = require("./nls"); @@ -36,10 +33,13 @@ function getTypeScriptCompilerOptions(src) { return options; } function createCompile(src, build, emitError) { + const tsb = require('gulp-tsb'); + const sourcemaps = require('gulp-sourcemaps'); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = Object.assign(Object.assign({}, getTypeScriptCompilerOptions(src)), { inlineSources: Boolean(build) }); const compilation = tsb.create(projectPath, overrideOptions, false, err => reporter(err)); function pipeline(token) { + const bom = require('gulp-bom'); const utf8Filter = util.filter(data => /(\/|\\)test(\/|\\).*utf8/.test(data.path)); const tsFilter = util.filter(data => /\.ts$/.test(data.path)); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); @@ -52,7 +52,7 @@ function createCompile(src, build, emitError) { .pipe(util.loadSourcemaps()) .pipe(compilation(token)) .pipe(noDeclarationsFilter) - .pipe(build ? nls() : es.through()) + .pipe(build ? nls.nls() : es.through()) .pipe(noDeclarationsFilter.restore) .pipe(sourcemaps.write('.', { addComment: false, diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 4f1260b09a..282dae530e 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -8,9 +8,6 @@ import * as es from 'event-stream'; import * as fs from 'fs'; import * as gulp from 'gulp'; -import * as bom from 'gulp-bom'; -import * as sourcemaps from 'gulp-sourcemaps'; -import * as tsb from 'gulp-tsb'; import * as path from 'path'; import * as monacodts from '../monaco/api'; import * as nls from './nls'; @@ -41,12 +38,17 @@ function getTypeScriptCompilerOptions(src: string): ts.CompilerOptions { } function createCompile(src: string, build: boolean, emitError?: boolean) { + const tsb = require('gulp-tsb') as typeof import('gulp-tsb'); + const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + + const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; const compilation = tsb.create(projectPath, overrideOptions, false, err => reporter(err)); function pipeline(token?: util.ICancellationToken) { + const bom = require('gulp-bom') as typeof import('gulp-bom'); const utf8Filter = util.filter(data => /(\/|\\)test(\/|\\).*utf8/.test(data.path)); const tsFilter = util.filter(data => /\.ts$/.test(data.path)); @@ -61,7 +63,7 @@ function createCompile(src: string, build: boolean, emitError?: boolean) { .pipe(util.loadSourcemaps()) .pipe(compilation(token)) .pipe(noDeclarationsFilter) - .pipe(build ? nls() : es.through()) + .pipe(build ? nls.nls() : es.through()) .pipe(noDeclarationsFilter.restore) .pipe(sourcemaps.write('.', { addComment: false, diff --git a/build/lib/dependencies.js b/build/lib/dependencies.js new file mode 100644 index 0000000000..31a8c9450b --- /dev/null +++ b/build/lib/dependencies.js @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getProductionDependencies = void 0; +const path = require("path"); +const cp = require("child_process"); +const _ = require("underscore"); +const parseSemver = require('parse-semver'); +function asYarnDependency(prefix, tree) { + let parseResult; + try { + parseResult = parseSemver(tree.name); + } + catch (err) { + err.message += `: ${tree.name}`; + console.warn(`Could not parse semver: ${tree.name}`); + return null; + } + // not an actual dependency in disk + if (parseResult.version !== parseResult.range) { + return null; + } + const name = parseResult.name; + const version = parseResult.version; + const dependencyPath = path.join(prefix, name); + const children = []; + for (const child of (tree.children || [])) { + const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child); + if (dep) { + children.push(dep); + } + } + return { name, version, path: dependencyPath, children }; +} +function getYarnProductionDependencies(cwd) { + const raw = cp.execSync('yarn list --json', { cwd, encoding: 'utf8', env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'production' }), stdio: [null, null, 'inherit'] }); + const match = /^{"type":"tree".*$/m.exec(raw); + if (!match || match.length !== 1) { + throw new Error('Could not parse result of `yarn list --json`'); + } + const trees = JSON.parse(match[0]).data.trees; + return trees + .map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree)) + .filter((dep) => !!dep); +} +function getProductionDependencies(cwd) { + const result = []; + const deps = getYarnProductionDependencies(cwd); + const flatten = (dep) => { result.push({ name: dep.name, version: dep.version, path: dep.path }); dep.children.forEach(flatten); }; + deps.forEach(flatten); + return _.uniq(result); +} +exports.getProductionDependencies = getProductionDependencies; +if (require.main === module) { + const root = path.dirname(path.dirname(__dirname)); + console.log(JSON.stringify(getProductionDependencies(root), null, ' ')); +} diff --git a/build/dependencies.js b/build/lib/dependencies.ts similarity index 64% rename from build/dependencies.js rename to build/lib/dependencies.ts index 02edf1b556..8292d92bdc 100644 --- a/build/dependencies.js +++ b/build/lib/dependencies.ts @@ -5,12 +5,27 @@ 'use strict'; -const path = require('path'); +import * as path from 'path'; +import * as cp from 'child_process'; +import * as _ from 'underscore'; const parseSemver = require('parse-semver'); -const cp = require('child_process'); -const _ = require('underscore'); -function asYarnDependency(prefix, tree) { +interface Tree { + readonly name: string; + readonly children?: Tree[]; +} + +interface FlatDependency { + readonly name: string; + readonly version: string; + readonly path: string; +} + +interface Dependency extends FlatDependency { + readonly children: Dependency[]; +} + +function asYarnDependency(prefix: string, tree: Tree): Dependency | null { let parseResult; try { @@ -42,7 +57,7 @@ function asYarnDependency(prefix, tree) { return { name, version, path: dependencyPath, children }; } -function getYarnProductionDependencies(cwd) { +function getYarnProductionDependencies(cwd: string): Dependency[] { const raw = cp.execSync('yarn list --json', { cwd, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, 'inherit'] }); const match = /^{"type":"tree".*$/m.exec(raw); @@ -50,25 +65,22 @@ function getYarnProductionDependencies(cwd) { throw new Error('Could not parse result of `yarn list --json`'); } - const trees = JSON.parse(match[0]).data.trees; + const trees = JSON.parse(match[0]).data.trees as Tree[]; return trees .map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree)) - .filter(dep => !!dep); + .filter((dep): dep is Dependency => !!dep); } -function getProductionDependencies(cwd) { - const result = []; +export function getProductionDependencies(cwd: string): FlatDependency[] { + const result: FlatDependency[] = []; const deps = getYarnProductionDependencies(cwd); - const flatten = dep => { result.push({ name: dep.name, version: dep.version, path: dep.path }); dep.children.forEach(flatten); }; + const flatten = (dep: Dependency) => { result.push({ name: dep.name, version: dep.version, path: dep.path }); dep.children.forEach(flatten); }; deps.forEach(flatten); - return _.uniq(result); } -module.exports.getProductionDependencies = getProductionDependencies; - if (require.main === module) { - const root = path.dirname(__dirname); + const root = path.dirname(path.dirname(__dirname)); console.log(JSON.stringify(getProductionDependencies(root), null, ' ')); } diff --git a/build/lib/electron.js b/build/lib/electron.js index 7cc8e981f5..8b5f459f18 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -9,10 +9,8 @@ const fs = require("fs"); const path = require("path"); const vfs = require("vinyl-fs"); const filter = require("gulp-filter"); -const json = require("gulp-json-editor"); const _ = require("underscore"); const util = require("./util"); -const electron = require('gulp-atom-electron'); const root = path.dirname(path.dirname(__dirname)); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const commit = util.getVersion(root); @@ -53,6 +51,8 @@ exports.config = { }; function getElectron(arch) { return () => { + const electron = require('gulp-atom-electron'); + const json = require('gulp-json-editor'); const electronOpts = _.extend({}, exports.config, { platform: process.platform, arch: arch === 'armhf' ? 'arm' : arch, diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 2aad4a99c7..daedd6670b 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -9,12 +9,9 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vfs from 'vinyl-fs'; import * as filter from 'gulp-filter'; -import * as json from 'gulp-json-editor'; import * as _ from 'underscore'; import * as util from './util'; -const electron = require('gulp-atom-electron'); - const root = path.dirname(path.dirname(__dirname)); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const commit = util.getVersion(root); @@ -59,6 +56,9 @@ export const config = { function getElectron(arch: string): () => NodeJS.ReadWriteStream { return () => { + const electron = require('gulp-atom-electron'); + const json = require('gulp-json-editor') as typeof import('gulp-json-editor'); + const electronOpts = _.extend({}, config, { platform: process.platform, arch: arch === 'armhf' ? 'arm' : arch, diff --git a/build/lib/eslint/vscode-dts-cancellation.js b/build/lib/eslint/vscode-dts-cancellation.js new file mode 100644 index 0000000000..32e59a2344 --- /dev/null +++ b/build/lib/eslint/vscode-dts-cancellation.js @@ -0,0 +1,33 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +module.exports = new class ApiProviderNaming { + constructor() { + this.meta = { + messages: { + noToken: 'Function lacks a cancellation token, preferable as last argument', + } + }; + } + create(context) { + return { + ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature[key.name=/^(provide|resolve).+/]']: (node) => { + let found = false; + for (let param of node.params) { + if (param.type === experimental_utils_1.AST_NODE_TYPES.Identifier) { + found = found || param.name === 'token'; + } + } + if (!found) { + context.report({ + node, + messageId: 'noToken' + }); + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-cancellation.ts b/build/lib/eslint/vscode-dts-cancellation.ts new file mode 100644 index 0000000000..b78d023b4f --- /dev/null +++ b/build/lib/eslint/vscode-dts-cancellation.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/experimental-utils'; + +export = new class ApiProviderNaming implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + noToken: 'Function lacks a cancellation token, preferable as last argument', + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + return { + ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature[key.name=/^(provide|resolve).+/]']: (node: any) => { + + let found = false; + for (let param of (node).params) { + if (param.type === AST_NODE_TYPES.Identifier) { + found = found || param.name === 'token'; + } + } + + if (!found) { + context.report({ + node, + messageId: 'noToken' + }); + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-provider-naming.js b/build/lib/eslint/vscode-dts-provider-naming.js new file mode 100644 index 0000000000..ffa86c524a --- /dev/null +++ b/build/lib/eslint/vscode-dts-provider-naming.js @@ -0,0 +1,38 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +module.exports = new (_a = class ApiProviderNaming { + constructor() { + this.meta = { + messages: { + naming: 'A provider should only have functions like provideXYZ or resolveXYZ', + } + }; + } + create(context) { + const config = context.options[0]; + const allowed = new Set(config.allowed); + return { + ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature']: (node) => { + var _a; + const interfaceName = ((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent).id.name; + if (allowed.has(interfaceName)) { + // allowed + return; + } + const methodName = node.key.name; + if (!ApiProviderNaming._providerFunctionNames.test(methodName)) { + context.report({ + node, + messageId: 'naming' + }); + } + } + }; + } + }, + _a._providerFunctionNames = /^(provide|resolve|prepare).+/, + _a); diff --git a/build/lib/eslint/vscode-dts-provider-naming.ts b/build/lib/eslint/vscode-dts-provider-naming.ts new file mode 100644 index 0000000000..e0f14d2604 --- /dev/null +++ b/build/lib/eslint/vscode-dts-provider-naming.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export = new class ApiProviderNaming implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + naming: 'A provider should only have functions like provideXYZ or resolveXYZ', + } + }; + + private static _providerFunctionNames = /^(provide|resolve|prepare).+/; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const config = <{ allowed: string[] }>context.options[0]; + const allowed = new Set(config.allowed); + + return { + ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature']: (node: any) => { + + + const interfaceName = ((node).parent?.parent).id.name; + if (allowed.has(interfaceName)) { + // allowed + return; + } + + const methodName = ((node).key).name; + + if (!ApiProviderNaming._providerFunctionNames.test(methodName)) { + context.report({ + node, + messageId: 'naming' + }); + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-region-comments.js b/build/lib/eslint/vscode-dts-region-comments.js new file mode 100644 index 0000000000..e3f29d75c7 --- /dev/null +++ b/build/lib/eslint/vscode-dts-region-comments.js @@ -0,0 +1,35 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +module.exports = new class ApiEventNaming { + constructor() { + this.meta = { + messages: { + comment: 'region comments should start with the GH issue link, e.g #region https://github.com/microsoft/vscode/issues/', + } + }; + } + create(context) { + const sourceCode = context.getSourceCode(); + return { + ['Program']: (_node) => { + for (let comment of sourceCode.getAllComments()) { + if (comment.type !== 'Line') { + continue; + } + if (!comment.value.match(/^\s*#region /)) { + continue; + } + if (!comment.value.match(/https:\/\/github.com\/microsoft\/vscode\/issues\/\d+/i)) { + context.report({ + node: comment, + messageId: 'comment', + }); + } + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-region-comments.ts b/build/lib/eslint/vscode-dts-region-comments.ts new file mode 100644 index 0000000000..c48d4ab443 --- /dev/null +++ b/build/lib/eslint/vscode-dts-region-comments.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; + +export = new class ApiEventNaming implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + comment: 'region comments should start with the GH issue link, e.g #region https://github.com/microsoft/vscode/issues/', + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const sourceCode = context.getSourceCode(); + + + return { + ['Program']: (_node: any) => { + + for (let comment of sourceCode.getAllComments()) { + if (comment.type !== 'Line') { + continue; + } + if (!comment.value.match(/^\s*#region /)) { + continue; + } + if (!comment.value.match(/https:\/\/github.com\/microsoft\/vscode\/issues\/\d+/i)) { + context.report({ + node: comment, + messageId: 'comment', + }); + } + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-use-thenable.js b/build/lib/eslint/vscode-dts-use-thenable.js new file mode 100644 index 0000000000..4111e50ff8 --- /dev/null +++ b/build/lib/eslint/vscode-dts-use-thenable.js @@ -0,0 +1,24 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +module.exports = new class ApiEventNaming { + constructor() { + this.meta = { + messages: { + usage: 'Use the Thenable-type instead of the Promise type', + } + }; + } + create(context) { + return { + ['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node) => { + context.report({ + node, + messageId: 'usage', + }); + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-use-thenable.ts b/build/lib/eslint/vscode-dts-use-thenable.ts new file mode 100644 index 0000000000..1344c2bc41 --- /dev/null +++ b/build/lib/eslint/vscode-dts-use-thenable.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; + +export = new class ApiEventNaming implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + usage: 'Use the Thenable-type instead of the Promise type', + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + + + return { + ['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node: any) => { + + context.report({ + node, + messageId: 'usage', + }); + } + }; + } +}; diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 77bcd668c8..a088357652 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -11,20 +11,15 @@ const glob = require("glob"); const gulp = require("gulp"); const path = require("path"); const File = require("vinyl"); -const vsce = require("vsce"); const stats_1 = require("./stats"); const util2 = require("./util"); -const remote = require("gulp-remote-retry-src"); const vzip = require('gulp-vinyl-zip'); const filter = require("gulp-filter"); const rename = require("gulp-rename"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const buffer = require('gulp-buffer'); -const json = require("gulp-json-editor"); const jsoncParser = require("jsonc-parser"); -const webpack = require('webpack'); -const webpackGulp = require('webpack-stream'); const util = require('./util'); const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); @@ -88,6 +83,9 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { } } } + const vsce = require('vsce'); + const webpack = require('webpack'); + const webpackGulp = require('webpack-stream'); vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn, packagedDependencies }).then(fileNames => { const files = fileNames .map(fileName => path.join(extensionPath, fileName)) @@ -149,6 +147,7 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { } function fromLocalNormal(extensionPath) { const result = es.through(); + const vsce = require('vsce'); vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn }) .then(fileNames => { const files = fileNames @@ -170,7 +169,8 @@ const baseHeaders = { 'X-Market-User-Id': '291C1CD0-051A-4123-9B4B-30D60EF52EE2', }; function fromMarketplace(extensionName, version, metadata) { - // {{SQL CARBON EDIT}} + const remote = require('gulp-remote-retry-src'); + const json = require('gulp-json-editor'); const [, name] = extensionName.split('.'); const url = `https://sqlopsextensions.blob.core.windows.net/extensions/${name}/${name}-${version}.vsix`; fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); @@ -233,9 +233,13 @@ const rebuildExtensions = [ 'big-data-cluster', 'mssql' ]; -const marketplaceWebExtensions = [ - 'ms-vscode.references-view' -]; +const marketplaceWebExtensionsExclude = new Set([ + 'ms-vscode.node-debug', + 'ms-vscode.node-debug2', + 'ms-vscode.js-debug-companion', + 'ms-vscode.js-debug', + 'ms-vscode.vscode-js-profile-table' +]); const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productJson.builtInExtensions || []; const webBuiltInExtensions = productJson.webBuiltInExtensions || []; @@ -279,7 +283,7 @@ function packageLocalExtensionsStream(forWeb) { exports.packageLocalExtensionsStream = packageLocalExtensionsStream; function packageMarketplaceExtensionsStream(forWeb) { const marketplaceExtensionsDescriptions = [ - ...builtInExtensions.filter(({ name }) => (forWeb ? marketplaceWebExtensions.indexOf(name) >= 0 : true)), + ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), ...(forWeb ? webBuiltInExtensions : []) ]; const marketplaceExtensionsStream = minifyExtensionResources(es.merge(...marketplaceExtensionsDescriptions diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 64f2daf3a0..24bbe9bd92 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -10,20 +10,15 @@ import * as gulp from 'gulp'; import * as path from 'path'; import { Stream } from 'stream'; import * as File from 'vinyl'; -import * as vsce from 'vsce'; import { createStatsStream } from './stats'; import * as util2 from './util'; -import remote = require('gulp-remote-retry-src'); const vzip = require('gulp-vinyl-zip'); import filter = require('gulp-filter'); import rename = require('gulp-rename'); import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; const buffer = require('gulp-buffer'); -import json = require('gulp-json-editor'); import * as jsoncParser from 'jsonc-parser'; -const webpack = require('webpack'); -const webpackGulp = require('webpack-stream'); const util = require('./util'); const root = path.dirname(path.dirname(__dirname)); const commit = util.getVersion(root); @@ -97,6 +92,10 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string): } } + const vsce = require('vsce') as typeof import('vsce'); + const webpack = require('webpack'); + const webpackGulp = require('webpack-stream'); + vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn, packagedDependencies }).then(fileNames => { const files = fileNames .map(fileName => path.join(extensionPath, fileName)) @@ -175,6 +174,8 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string): function fromLocalNormal(extensionPath: string): Stream { const result = es.through(); + const vsce = require('vsce') as typeof import('vsce'); + vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Yarn }) .then(fileNames => { const files = fileNames @@ -200,7 +201,8 @@ const baseHeaders = { }; export function fromMarketplace(extensionName: string, version: string, metadata: any): Stream { - // {{SQL CARBON EDIT}} + const json = require('gulp-json-editor') as typeof import('gulp-json-editor'); + const [, name] = extensionName.split('.'); const url = `https://sqlopsextensions.blob.core.windows.net/extensions/${name}/${name}-${version}.vsix`; @@ -269,9 +271,13 @@ const rebuildExtensions = [ 'mssql' ]; -const marketplaceWebExtensions = [ - 'ms-vscode.references-view' -]; +const marketplaceWebExtensionsExclude = new Set([ + 'ms-vscode.node-debug', + 'ms-vscode.node-debug2', + 'ms-vscode.js-debug-companion', + 'ms-vscode.js-debug', + 'ms-vscode.vscode-js-profile-table' +]); interface IBuiltInExtension { name: string; @@ -339,7 +345,7 @@ export function packageLocalExtensionsStream(forWeb: boolean): Stream { export function packageMarketplaceExtensionsStream(forWeb: boolean): Stream { const marketplaceExtensionsDescriptions = [ - ...builtInExtensions.filter(({ name }) => (forWeb ? marketplaceWebExtensions.indexOf(name) >= 0 : true)), + ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), ...(forWeb ? webBuiltInExtensions : []) ]; const marketplaceExtensionsStream = minifyExtensionResources( diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 9ec898986a..bb2ed8a7e4 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -182,6 +182,10 @@ "name": "vs/workbench/contrib/tasks", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/testing", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/terminal", "project": "vscode-workbench" @@ -222,6 +226,10 @@ "name": "vs/workbench/contrib/customEditor", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/externalUriOpener", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/welcome", "project": "vscode-workbench" @@ -250,6 +258,10 @@ "name": "vs/workbench/services/bulkEdit", "project": "vscode-workbench" }, + { + "name": "vs/workbench/services/clipboard", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/commands", "project": "vscode-workbench" diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 6293acaabf..00360b4099 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -24,8 +24,6 @@ const minimatch_1 = require("minimatch"); // Feel free to add more core types as you see needed if present in node.js and browsers const CORE_TYPES = [ 'require', - // 'atob', - // 'btoa', 'setTimeout', 'clearTimeout', 'setInterval', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index fe899c6683..2c162875f6 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -25,8 +25,6 @@ import { match } from 'minimatch'; // Feel free to add more core types as you see needed if present in node.js and browsers const CORE_TYPES = [ 'require', // from our AMD loader - // 'atob', - // 'btoa', 'setTimeout', 'clearTimeout', 'setInterval', diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js index a8ebfe51ac..86cf8a6174 100644 --- a/build/lib/monaco-api.js +++ b/build/lib/monaco-api.js @@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.execute = exports.run3 = exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; const fs = require("fs"); -const ts = require("typescript"); const path = require("path"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); @@ -18,7 +17,7 @@ const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts'); function logErr(message, ...rest) { fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest); } -function isDeclaration(a) { +function isDeclaration(ts, a) { return (a.kind === ts.SyntaxKind.InterfaceDeclaration || a.kind === ts.SyntaxKind.EnumDeclaration || a.kind === ts.SyntaxKind.ClassDeclaration @@ -26,7 +25,7 @@ function isDeclaration(a) { || a.kind === ts.SyntaxKind.FunctionDeclaration || a.kind === ts.SyntaxKind.ModuleDeclaration); } -function visitTopLevelDeclarations(sourceFile, visitor) { +function visitTopLevelDeclarations(ts, sourceFile, visitor) { let stop = false; let visit = (node) => { if (stop) { @@ -49,9 +48,9 @@ function visitTopLevelDeclarations(sourceFile, visitor) { }; visit(sourceFile); } -function getAllTopLevelDeclarations(sourceFile) { +function getAllTopLevelDeclarations(ts, sourceFile) { let all = []; - visitTopLevelDeclarations(sourceFile, (node) => { + visitTopLevelDeclarations(ts, sourceFile, (node) => { if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) { let interfaceDeclaration = node; let triviaStart = interfaceDeclaration.pos; @@ -71,10 +70,10 @@ function getAllTopLevelDeclarations(sourceFile) { }); return all; } -function getTopLevelDeclaration(sourceFile, typeName) { +function getTopLevelDeclaration(ts, sourceFile, typeName) { let result = null; - visitTopLevelDeclarations(sourceFile, (node) => { - if (isDeclaration(node) && node.name) { + visitTopLevelDeclarations(ts, sourceFile, (node) => { + if (isDeclaration(ts, node) && node.name) { if (node.name.text === typeName) { result = node; return true /*stop*/; @@ -104,18 +103,18 @@ function hasModifier(modifiers, kind) { } return false; } -function isStatic(member) { +function isStatic(ts, member) { return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); } -function isDefaultExport(declaration) { +function isDefaultExport(ts, declaration) { return (hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword)); } -function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage, enums) { +function getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums) { let result = getNodeText(sourceFile, declaration); if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { let interfaceDeclaration = declaration; - const staticTypeName = (isDefaultExport(interfaceDeclaration) + const staticTypeName = (isDefaultExport(ts, interfaceDeclaration) ? `${importName}.default` : `${importName}.${declaration.name.text}`); let instanceTypeName = staticTypeName; @@ -137,7 +136,7 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, else { const memberName = member.name.text; const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); - if (isStatic(member)) { + if (isStatic(ts, member)) { usage.push(`a = ${staticTypeName}${memberAccess};`); } else { @@ -191,7 +190,7 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, } return result; } -function format(text, endl) { +function format(ts, text, endl) { const REALLY_FORMAT = false; text = preformat(text, endl); if (!REALLY_FORMAT) { @@ -336,7 +335,7 @@ function createReplacer(data) { }); return createReplacerFromDirectives(directives); } -function generateDeclarationFile(recipe, sourceFileGetter) { +function generateDeclarationFile(ts, recipe, sourceFileGetter) { const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; let lines = recipe.split(endl); let result = []; @@ -379,14 +378,14 @@ function generateDeclarationFile(recipe, sourceFileGetter) { if (typeName.length === 0) { return; } - let declaration = getTopLevelDeclaration(sourceFile, typeName); + let declaration = getTopLevelDeclaration(ts, sourceFile, typeName); if (!declaration) { logErr(`While handling ${line}`); logErr(`Cannot find ${typeName}`); failed = true; return; } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage, enums))); + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); }); return; } @@ -413,8 +412,8 @@ function generateDeclarationFile(recipe, sourceFileGetter) { typesToExcludeMap[typeName] = true; typesToExcludeArr.push(typeName); }); - getAllTopLevelDeclarations(sourceFile).forEach((declaration) => { - if (isDeclaration(declaration) && declaration.name) { + getAllTopLevelDeclarations(ts, sourceFile).forEach((declaration) => { + if (isDeclaration(ts, declaration) && declaration.name) { if (typesToExcludeMap[declaration.name.text]) { return; } @@ -428,7 +427,7 @@ function generateDeclarationFile(recipe, sourceFileGetter) { } } } - result.push(replacer(getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, usage, enums))); + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); }); return; } @@ -450,7 +449,7 @@ function generateDeclarationFile(recipe, sourceFileGetter) { resultTxt = resultTxt.replace(/\bURI\b/g, 'Uri'); resultTxt = resultTxt.replace(/\bEvent { if (e1.enumName < e2.enumName) { @@ -471,7 +470,7 @@ function generateDeclarationFile(recipe, sourceFileGetter) { '' ].concat(enums.map(e => e.text)).join(endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); - resultEnums = format(resultEnums, endl); + resultEnums = format(ts, resultEnums, endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); return { result: resultTxt, @@ -479,9 +478,9 @@ function generateDeclarationFile(recipe, sourceFileGetter) { enums: resultEnums }; } -function _run(sourceFileGetter) { +function _run(ts, sourceFileGetter) { const recipe = fs.readFileSync(exports.RECIPE_PATH).toString(); - const t = generateDeclarationFile(recipe, sourceFileGetter); + const t = generateDeclarationFile(ts, recipe, sourceFileGetter); if (!t) { return null; } @@ -521,6 +520,7 @@ class CacheEntry { class DeclarationResolver { constructor(_fsProvider) { this._fsProvider = _fsProvider; + this.ts = require('typescript'); this._sourceFileCache = Object.create(null); } invalidateCache(moduleId) { @@ -555,25 +555,26 @@ class DeclarationResolver { if (/\.d\.ts$/.test(moduleId)) { // const mtime = this._fsProvider.statFileSync() const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); - return new CacheEntry(ts.createSourceFile(fileName, fileContents, ts.ScriptTarget.ES5), mtime); + return new CacheEntry(this.ts.createSourceFile(fileName, fileContents, this.ts.ScriptTarget.ES5), mtime); } const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); const fileMap = { 'file.ts': fileContents }; - const service = ts.createLanguageService(new TypeScriptLanguageServiceHost({}, fileMap, {})); + const service = this.ts.createLanguageService(new TypeScriptLanguageServiceHost(this.ts, {}, fileMap, {})); const text = service.getEmitOutput('file.ts', true, true).outputFiles[0].text; - return new CacheEntry(ts.createSourceFile(fileName, text, ts.ScriptTarget.ES5), mtime); + return new CacheEntry(this.ts.createSourceFile(fileName, text, this.ts.ScriptTarget.ES5), mtime); } } exports.DeclarationResolver = DeclarationResolver; function run3(resolver) { const sourceFileGetter = (moduleId) => resolver.getDeclarationSourceFile(moduleId); - return _run(sourceFileGetter); + return _run(resolver.ts, sourceFileGetter); } exports.run3 = run3; class TypeScriptLanguageServiceHost { - constructor(libs, files, compilerOptions) { + constructor(ts, libs, files, compilerOptions) { + this._ts = ts; this._libs = libs; this._files = files; this._compilerOptions = compilerOptions; @@ -595,17 +596,17 @@ class TypeScriptLanguageServiceHost { } getScriptSnapshot(fileName) { if (this._files.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._files[fileName]); + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); } else if (this._libs.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._libs[fileName]); + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); } else { - return ts.ScriptSnapshot.fromString(''); + return this._ts.ScriptSnapshot.fromString(''); } } getScriptKind(_fileName) { - return ts.ScriptKind.TS; + return this._ts.ScriptKind.TS; } getCurrentDirectory() { return ''; diff --git a/build/lib/monaco-api.ts b/build/lib/monaco-api.ts new file mode 100644 index 0000000000..126153ef8f --- /dev/null +++ b/build/lib/monaco-api.ts @@ -0,0 +1,753 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import type * as ts from 'typescript'; +import * as path from 'path'; +import * as fancyLog from 'fancy-log'; +import * as ansiColors from 'ansi-colors'; + +const dtsv = '3'; + +const tsfmt = require('../../tsfmt.json'); + +const SRC = path.join(__dirname, '../../src'); +export const RECIPE_PATH = path.join(__dirname, '../monaco/monaco.d.ts.recipe'); +const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts'); + +function logErr(message: any, ...rest: any[]): void { + fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest); +} + +type SourceFileGetter = (moduleId: string) => ts.SourceFile | null; + +type TSTopLevelDeclaration = ts.InterfaceDeclaration | ts.EnumDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration | ts.ModuleDeclaration; +type TSTopLevelDeclare = TSTopLevelDeclaration | ts.VariableStatement; + +function isDeclaration(ts: typeof import('typescript'), a: TSTopLevelDeclare): a is TSTopLevelDeclaration { + return ( + a.kind === ts.SyntaxKind.InterfaceDeclaration + || a.kind === ts.SyntaxKind.EnumDeclaration + || a.kind === ts.SyntaxKind.ClassDeclaration + || a.kind === ts.SyntaxKind.TypeAliasDeclaration + || a.kind === ts.SyntaxKind.FunctionDeclaration + || a.kind === ts.SyntaxKind.ModuleDeclaration + ); +} + +function visitTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: ts.SourceFile, visitor: (node: TSTopLevelDeclare) => boolean): void { + let stop = false; + + let visit = (node: ts.Node): void => { + if (stop) { + return; + } + + switch (node.kind) { + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.VariableStatement: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + stop = visitor(node); + } + + if (stop) { + return; + } + ts.forEachChild(node, visit); + }; + + visit(sourceFile); +} + + +function getAllTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: ts.SourceFile): TSTopLevelDeclare[] { + let all: TSTopLevelDeclare[] = []; + visitTopLevelDeclarations(ts, sourceFile, (node) => { + if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) { + let interfaceDeclaration = node; + let triviaStart = interfaceDeclaration.pos; + let triviaEnd = interfaceDeclaration.name.pos; + let triviaText = getNodeText(sourceFile, { pos: triviaStart, end: triviaEnd }); + + if (triviaText.indexOf('@internal') === -1) { + all.push(node); + } + } else { + let nodeText = getNodeText(sourceFile, node); + if (nodeText.indexOf('@internal') === -1) { + all.push(node); + } + } + return false /*continue*/; + }); + return all; +} + + +function getTopLevelDeclaration(ts: typeof import('typescript'), sourceFile: ts.SourceFile, typeName: string): TSTopLevelDeclare | null { + let result: TSTopLevelDeclare | null = null; + visitTopLevelDeclarations(ts, sourceFile, (node) => { + if (isDeclaration(ts, node) && node.name) { + if (node.name.text === typeName) { + result = node; + return true /*stop*/; + } + return false /*continue*/; + } + // node is ts.VariableStatement + if (getNodeText(sourceFile, node).indexOf(typeName) >= 0) { + result = node; + return true /*stop*/; + } + return false /*continue*/; + }); + return result; +} + + +function getNodeText(sourceFile: ts.SourceFile, node: { pos: number; end: number; }): string { + return sourceFile.getFullText().substring(node.pos, node.end); +} + +function hasModifier(modifiers: ts.NodeArray | undefined, kind: ts.SyntaxKind): boolean { + if (modifiers) { + for (let i = 0; i < modifiers.length; i++) { + let mod = modifiers[i]; + if (mod.kind === kind) { + return true; + } + } + } + return false; +} + +function isStatic(ts: typeof import('typescript'), member: ts.ClassElement | ts.TypeElement): boolean { + return hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword); +} + +function isDefaultExport(ts: typeof import('typescript'), declaration: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean { + return ( + hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) + && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword) + ); +} + +function getMassagedTopLevelDeclarationText(ts: typeof import('typescript'), sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[], enums: IEnumEntry[]): string { + let result = getNodeText(sourceFile, declaration); + if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { + let interfaceDeclaration = declaration; + + const staticTypeName = ( + isDefaultExport(ts, interfaceDeclaration) + ? `${importName}.default` + : `${importName}.${declaration.name!.text}` + ); + + let instanceTypeName = staticTypeName; + const typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); + if (typeParametersCnt > 0) { + let arr: string[] = []; + for (let i = 0; i < typeParametersCnt; i++) { + arr.push('any'); + } + instanceTypeName = `${instanceTypeName}<${arr.join(',')}>`; + } + + const members: ts.NodeArray = interfaceDeclaration.members; + members.forEach((member) => { + try { + let memberText = getNodeText(sourceFile, member); + if (memberText.indexOf('@internal') >= 0 || memberText.indexOf('private') >= 0) { + result = result.replace(memberText, ''); + } else { + const memberName = (member.name).text; + const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); + if (isStatic(ts, member)) { + usage.push(`a = ${staticTypeName}${memberAccess};`); + } else { + usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`); + } + } + } catch (err) { + // life.. + } + }); + } else if (declaration.kind === ts.SyntaxKind.VariableStatement) { + const jsDoc = result.substr(0, declaration.getLeadingTriviaWidth(sourceFile)); + if (jsDoc.indexOf('@monacodtsreplace') >= 0) { + const jsDocLines = jsDoc.split(/\r\n|\r|\n/); + let directives: [RegExp, string][] = []; + for (const jsDocLine of jsDocLines) { + const m = jsDocLine.match(/^\s*\* \/([^/]+)\/([^/]+)\/$/); + if (m) { + directives.push([new RegExp(m[1], 'g'), m[2]]); + } + } + // remove the jsdoc + result = result.substr(jsDoc.length); + if (directives.length > 0) { + // apply replace directives + const replacer = createReplacerFromDirectives(directives); + result = replacer(result); + } + } + } + result = result.replace(/export default /g, 'export '); + result = result.replace(/export declare /g, 'export '); + result = result.replace(/declare /g, ''); + let lines = result.split(/\r\n|\r|\n/); + for (let i = 0; i < lines.length; i++) { + if (/\s*\*/.test(lines[i])) { + // very likely a comment + continue; + } + lines[i] = lines[i].replace(/"/g, '\''); + } + result = lines.join('\n'); + + if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { + result = result.replace(/const enum/, 'enum'); + enums.push({ + enumName: declaration.name.getText(sourceFile), + text: result + }); + } + + return result; +} + +function format(ts: typeof import('typescript'), text: string, endl: string): string { + const REALLY_FORMAT = false; + + text = preformat(text, endl); + if (!REALLY_FORMAT) { + return text; + } + + // Parse the source text + let sourceFile = ts.createSourceFile('file.ts', text, ts.ScriptTarget.Latest, /*setParentPointers*/ true); + + // Get the formatting edits on the input sources + let edits = (ts).formatting.formatDocument(sourceFile, getRuleProvider(tsfmt), tsfmt); + + // Apply the edits on the input code + return applyEdits(text, edits); + + function countParensCurly(text: string): number { + let cnt = 0; + for (let i = 0; i < text.length; i++) { + if (text.charAt(i) === '(' || text.charAt(i) === '{') { + cnt++; + } + if (text.charAt(i) === ')' || text.charAt(i) === '}') { + cnt--; + } + } + return cnt; + } + + function repeatStr(s: string, cnt: number): string { + let r = ''; + for (let i = 0; i < cnt; i++) { + r += s; + } + return r; + } + + function preformat(text: string, endl: string): string { + let lines = text.split(endl); + let inComment = false; + let inCommentDeltaIndent = 0; + let indent = 0; + for (let i = 0; i < lines.length; i++) { + let line = lines[i].replace(/\s$/, ''); + let repeat = false; + let lineIndent = 0; + do { + repeat = false; + if (line.substring(0, 4) === ' ') { + line = line.substring(4); + lineIndent++; + repeat = true; + } + if (line.charAt(0) === '\t') { + line = line.substring(1); + lineIndent++; + repeat = true; + } + } while (repeat); + + if (line.length === 0) { + continue; + } + + if (inComment) { + if (/\*\//.test(line)) { + inComment = false; + } + lines[i] = repeatStr('\t', lineIndent + inCommentDeltaIndent) + line; + continue; + } + + if (/\/\*/.test(line)) { + inComment = true; + inCommentDeltaIndent = indent - lineIndent; + lines[i] = repeatStr('\t', indent) + line; + continue; + } + + const cnt = countParensCurly(line); + let shouldUnindentAfter = false; + let shouldUnindentBefore = false; + if (cnt < 0) { + if (/[({]/.test(line)) { + shouldUnindentAfter = true; + } else { + shouldUnindentBefore = true; + } + } else if (cnt === 0) { + shouldUnindentBefore = /^\}/.test(line); + } + let shouldIndentAfter = false; + if (cnt > 0) { + shouldIndentAfter = true; + } else if (cnt === 0) { + shouldIndentAfter = /{$/.test(line); + } + + if (shouldUnindentBefore) { + indent--; + } + + lines[i] = repeatStr('\t', indent) + line; + + if (shouldUnindentAfter) { + indent--; + } + if (shouldIndentAfter) { + indent++; + } + } + return lines.join(endl); + } + + function getRuleProvider(options: ts.FormatCodeSettings) { + // Share this between multiple formatters using the same options. + // This represents the bulk of the space the formatter uses. + return (ts as any).formatting.getFormatContext(options); + } + + function applyEdits(text: string, edits: ts.TextChange[]): string { + // Apply edits in reverse on the existing text + let result = text; + for (let i = edits.length - 1; i >= 0; i--) { + let change = edits[i]; + let head = result.slice(0, change.span.start); + let tail = result.slice(change.span.start + change.span.length); + result = head + change.newText + tail; + } + return result; + } +} + +function createReplacerFromDirectives(directives: [RegExp, string][]): (str: string) => string { + return (str: string) => { + for (let i = 0; i < directives.length; i++) { + str = str.replace(directives[i][0], directives[i][1]); + } + return str; + }; +} + +function createReplacer(data: string): (str: string) => string { + data = data || ''; + let rawDirectives = data.split(';'); + let directives: [RegExp, string][] = []; + rawDirectives.forEach((rawDirective) => { + if (rawDirective.length === 0) { + return; + } + let pieces = rawDirective.split('=>'); + let findStr = pieces[0]; + let replaceStr = pieces[1]; + + findStr = findStr.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&'); + findStr = '\\b' + findStr + '\\b'; + directives.push([new RegExp(findStr, 'g'), replaceStr]); + }); + + return createReplacerFromDirectives(directives); +} + +interface ITempResult { + result: string; + usageContent: string; + enums: string; +} + +interface IEnumEntry { + enumName: string; + text: string; +} + +function generateDeclarationFile(ts: typeof import('typescript'), recipe: string, sourceFileGetter: SourceFileGetter): ITempResult | null { + const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; + + let lines = recipe.split(endl); + let result: string[] = []; + + let usageCounter = 0; + let usageImports: string[] = []; + let usage: string[] = []; + + let failed = false; + + usage.push(`var a: any;`); + usage.push(`var b: any;`); + + const generateUsageImport = (moduleId: string) => { + let importName = 'm' + (++usageCounter); + usageImports.push(`import * as ${importName} from './${moduleId.replace(/\.d\.ts$/, '')}';`); + return importName; + }; + + let enums: IEnumEntry[] = []; + let version: string | null = null; + + lines.forEach(line => { + + if (failed) { + return; + } + + let m0 = line.match(/^\/\/dtsv=(\d+)$/); + if (m0) { + version = m0[1]; + } + + let m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); + if (m1) { + let moduleId = m1[1]; + const sourceFile = sourceFileGetter(moduleId); + if (!sourceFile) { + logErr(`While handling ${line}`); + logErr(`Cannot find ${moduleId}`); + failed = true; + return; + } + + const importName = generateUsageImport(moduleId); + + let replacer = createReplacer(m1[2]); + + let typeNames = m1[3].split(/,/); + typeNames.forEach((typeName) => { + typeName = typeName.trim(); + if (typeName.length === 0) { + return; + } + let declaration = getTopLevelDeclaration(ts, sourceFile, typeName); + if (!declaration) { + logErr(`While handling ${line}`); + logErr(`Cannot find ${typeName}`); + failed = true; + return; + } + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); + }); + return; + } + + let m2 = line.match(/^\s*#includeAll\(([^;)]*)(;[^)]*)?\)\:(.*)$/); + if (m2) { + let moduleId = m2[1]; + const sourceFile = sourceFileGetter(moduleId); + if (!sourceFile) { + logErr(`While handling ${line}`); + logErr(`Cannot find ${moduleId}`); + failed = true; + return; + } + + const importName = generateUsageImport(moduleId); + + let replacer = createReplacer(m2[2]); + + let typeNames = m2[3].split(/,/); + let typesToExcludeMap: { [typeName: string]: boolean; } = {}; + let typesToExcludeArr: string[] = []; + typeNames.forEach((typeName) => { + typeName = typeName.trim(); + if (typeName.length === 0) { + return; + } + typesToExcludeMap[typeName] = true; + typesToExcludeArr.push(typeName); + }); + + getAllTopLevelDeclarations(ts, sourceFile).forEach((declaration) => { + if (isDeclaration(ts, declaration) && declaration.name) { + if (typesToExcludeMap[declaration.name.text]) { + return; + } + } else { + // node is ts.VariableStatement + let nodeText = getNodeText(sourceFile, declaration); + for (let i = 0; i < typesToExcludeArr.length; i++) { + if (nodeText.indexOf(typesToExcludeArr[i]) >= 0) { + return; + } + } + } + result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); + }); + return; + } + + result.push(line); + }); + + if (failed) { + return null; + } + + if (version !== dtsv) { + if (!version) { + logErr(`gulp watch restart required. 'monaco.d.ts.recipe' is written before versioning was introduced.`); + } else { + logErr(`gulp watch restart required. 'monaco.d.ts.recipe' v${version} does not match runtime v${dtsv}.`); + } + return null; + } + + let resultTxt = result.join(endl); + resultTxt = resultTxt.replace(/\bURI\b/g, 'Uri'); + resultTxt = resultTxt.replace(/\bEvent { + if (e1.enumName < e2.enumName) { + return -1; + } + if (e1.enumName > e2.enumName) { + return 1; + } + return 0; + }); + + let resultEnums = [ + '/*---------------------------------------------------------------------------------------------', + ' * Copyright (c) Microsoft Corporation. All rights reserved.', + ' * Licensed under the Source EULA. See License.txt in the project root for license information.', + ' *--------------------------------------------------------------------------------------------*/', + '', + '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', + '' + ].concat(enums.map(e => e.text)).join(endl); + resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); + resultEnums = format(ts, resultEnums, endl); + resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); + + return { + result: resultTxt, + usageContent: `${usageImports.join('\n')}\n\n${usage.join('\n')}`, + enums: resultEnums + }; +} + +export interface IMonacoDeclarationResult { + content: string; + usageContent: string; + enums: string; + filePath: string; + isTheSame: boolean; +} + +function _run(ts: typeof import('typescript'), sourceFileGetter: SourceFileGetter): IMonacoDeclarationResult | null { + const recipe = fs.readFileSync(RECIPE_PATH).toString(); + const t = generateDeclarationFile(ts, recipe, sourceFileGetter); + if (!t) { + return null; + } + + const result = t.result; + const usageContent = t.usageContent; + const enums = t.enums; + + const currentContent = fs.readFileSync(DECLARATION_PATH).toString(); + const one = currentContent.replace(/\r\n/gm, '\n'); + const other = result.replace(/\r\n/gm, '\n'); + const isTheSame = (one === other); + + return { + content: result, + usageContent: usageContent, + enums: enums, + filePath: DECLARATION_PATH, + isTheSame + }; +} + +export class FSProvider { + public existsSync(filePath: string): boolean { + return fs.existsSync(filePath); + } + public statSync(filePath: string): fs.Stats { + return fs.statSync(filePath); + } + public readFileSync(_moduleId: string, filePath: string): Buffer { + return fs.readFileSync(filePath); + } +} + +class CacheEntry { + constructor( + public readonly sourceFile: ts.SourceFile, + public readonly mtime: number + ) {} +} + +export class DeclarationResolver { + + public readonly ts: typeof import('typescript'); + private _sourceFileCache: { [moduleId: string]: CacheEntry | null; }; + + constructor(private readonly _fsProvider: FSProvider) { + this.ts = require('typescript') as typeof import('typescript'); + this._sourceFileCache = Object.create(null); + } + + public invalidateCache(moduleId: string): void { + this._sourceFileCache[moduleId] = null; + } + + public getDeclarationSourceFile(moduleId: string): ts.SourceFile | null { + if (this._sourceFileCache[moduleId]) { + // Since we cannot trust file watching to invalidate the cache, check also the mtime + const fileName = this._getFileName(moduleId); + const mtime = this._fsProvider.statSync(fileName).mtime.getTime(); + if (this._sourceFileCache[moduleId]!.mtime !== mtime) { + this._sourceFileCache[moduleId] = null; + } + } + if (!this._sourceFileCache[moduleId]) { + this._sourceFileCache[moduleId] = this._getDeclarationSourceFile(moduleId); + } + return this._sourceFileCache[moduleId] ? this._sourceFileCache[moduleId]!.sourceFile : null; + } + + private _getFileName(moduleId: string): string { + if (/\.d\.ts$/.test(moduleId)) { + return path.join(SRC, moduleId); + } + return path.join(SRC, `${moduleId}.ts`); + } + + private _getDeclarationSourceFile(moduleId: string): CacheEntry | null { + const fileName = this._getFileName(moduleId); + if (!this._fsProvider.existsSync(fileName)) { + return null; + } + const mtime = this._fsProvider.statSync(fileName).mtime.getTime(); + if (/\.d\.ts$/.test(moduleId)) { + // const mtime = this._fsProvider.statFileSync() + const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); + return new CacheEntry( + this.ts.createSourceFile(fileName, fileContents, this.ts.ScriptTarget.ES5), + mtime + ); + } + const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); + const fileMap: IFileMap = { + 'file.ts': fileContents + }; + const service = this.ts.createLanguageService(new TypeScriptLanguageServiceHost(this.ts, {}, fileMap, {})); + const text = service.getEmitOutput('file.ts', true, true).outputFiles[0].text; + return new CacheEntry( + this.ts.createSourceFile(fileName, text, this.ts.ScriptTarget.ES5), + mtime + ); + } +} + +export function run3(resolver: DeclarationResolver): IMonacoDeclarationResult | null { + const sourceFileGetter = (moduleId: string) => resolver.getDeclarationSourceFile(moduleId); + return _run(resolver.ts, sourceFileGetter); +} + + + + +interface ILibMap { [libName: string]: string; } +interface IFileMap { [fileName: string]: string; } + +class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + + private readonly _ts: typeof import('typescript'); + private readonly _libs: ILibMap; + private readonly _files: IFileMap; + private readonly _compilerOptions: ts.CompilerOptions; + + constructor(ts: typeof import('typescript'), libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._ts = ts; + this._libs = libs; + this._files = files; + this._compilerOptions = compilerOptions; + } + + // --- language service host --------------- + + getCompilationSettings(): ts.CompilerOptions { + return this._compilerOptions; + } + getScriptFileNames(): string[] { + return ( + ([] as string[]) + .concat(Object.keys(this._libs)) + .concat(Object.keys(this._files)) + ); + } + getScriptVersion(_fileName: string): string { + return '1'; + } + getProjectVersion(): string { + return '1'; + } + getScriptSnapshot(fileName: string): ts.IScriptSnapshot { + if (this._files.hasOwnProperty(fileName)) { + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); + } else if (this._libs.hasOwnProperty(fileName)) { + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); + } else { + return this._ts.ScriptSnapshot.fromString(''); + } + } + getScriptKind(_fileName: string): ts.ScriptKind { + return this._ts.ScriptKind.TS; + } + getCurrentDirectory(): string { + return ''; + } + getDefaultLibFileName(_options: ts.CompilerOptions): string { + return 'defaultLib:es5'; + } + isDefaultLibFileName(fileName: string): boolean { + return fileName === this.getDefaultLibFileName(this._compilerOptions); + } +} + +export function execute(): IMonacoDeclarationResult { + let r = run3(new DeclarationResolver(new FSProvider())); + if (!r) { + throw new Error(`monaco.d.ts generation error - Cannot continue`); + } + return r; +} diff --git a/build/lib/nls.js b/build/lib/nls.js index b530196fc8..710d3061c3 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -3,7 +3,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const ts = require("typescript"); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.nls = void 0; const lazy = require("lazy.js"); const event_stream_1 = require("event-stream"); const File = require("vinyl"); @@ -16,7 +17,7 @@ var CollectStepResult; CollectStepResult[CollectStepResult["No"] = 2] = "No"; CollectStepResult[CollectStepResult["NoAndRecurse"] = 3] = "NoAndRecurse"; })(CollectStepResult || (CollectStepResult = {})); -function collect(node, fn) { +function collect(ts, node, fn) { const result = []; function loop(node) { const stepResult = fn(node); @@ -69,14 +70,16 @@ function nls() { if (!typescript) { return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); } - nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); })); return event_stream_1.duplex(input, output); } -function isImportNode(node) { +exports.nls = nls; +function isImportNode(ts, node) { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } -(function (nls_1) { +var _nls; +(function (_nls) { function fileFrom(file, contents, path = file.path) { return new File({ contents: Buffer.from(contents), @@ -85,17 +88,14 @@ function isImportNode(node) { path: path }); } - nls_1.fileFrom = fileFrom; function mappedPositionFrom(source, lc) { return { source, line: lc.line + 1, column: lc.character }; } - nls_1.mappedPositionFrom = mappedPositionFrom; function lcFrom(position) { return { line: position.line - 1, character: position.column }; } - nls_1.lcFrom = lcFrom; class SingleFileServiceHost { - constructor(options, filename, contents) { + constructor(ts, options, filename, contents) { this.options = options; this.filename = filename; this.getCompilationSettings = () => this.options; @@ -108,20 +108,19 @@ function isImportNode(node) { this.lib = ts.ScriptSnapshot.fromString(''); } } - nls_1.SingleFileServiceHost = SingleFileServiceHost; - function isCallExpressionWithinTextSpanCollectStep(textSpan, node) { + function isCallExpressionWithinTextSpanCollectStep(ts, textSpan, node) { if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { return CollectStepResult.No; } return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; } - function analyze(contents, options = {}) { + function analyze(ts, contents, options = {}) { const filename = 'file.ts'; - const serviceHost = new SingleFileServiceHost(Object.assign(clone(options), { noResolve: true }), filename, contents); + const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents); const service = ts.createLanguageService(serviceHost); const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); // all imports - const imports = lazy(collect(sourceFile, n => isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); + const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); // import nls = require('vs/nls'); const importEqualsDeclarations = imports .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) @@ -152,7 +151,7 @@ function isImportNode(node) { .flatten() .filter(r => !r.isWriteAccess) // find the deepest call expressions AST nodes that contain those references - .map(r => collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(r.textSpan, n))) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) .map(n => n) @@ -178,7 +177,7 @@ function isImportNode(node) { // find the deepest call expressions AST nodes that contain those references const localizeCallExpressions = localizeReferences .concat(namedLocalizeReferences) - .map(r => collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(r.textSpan, n))) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) .map(n => n); @@ -199,7 +198,6 @@ function isImportNode(node) { nlsExpressions: nlsExpressions.toArray() }; } - nls_1.analyze = analyze; class TextModel { constructor(contents) { const regex = /\r\n|\r|\n/g; @@ -250,9 +248,8 @@ function isImportNode(node) { .flatten().toArray().join(''); } } - nls_1.TextModel = TextModel; function patchJavascript(patches, contents, moduleId) { - const model = new nls.TextModel(contents); + const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); // patch the 'vs/nls' imports @@ -261,7 +258,6 @@ function isImportNode(node) { model.set(0, patchedFirstLine); return model.toString(); } - nls_1.patchJavascript = patchJavascript; function patchSourcemap(patches, rsm, smc) { const smg = new sm.SourceMapGenerator({ file: rsm.file, @@ -297,9 +293,8 @@ function isImportNode(node) { } return JSON.parse(smg.toString()); } - nls_1.patchSourcemap = patchSourcemap; - function patch(moduleId, typescript, javascript, sourcemap) { - const { localizeCalls, nlsExpressions } = analyze(typescript); + function patch(ts, moduleId, typescript, javascript, sourcemap) { + const { localizeCalls, nlsExpressions } = analyze(ts, typescript); if (localizeCalls.length === 0) { return { javascript, sourcemap }; } @@ -332,13 +327,13 @@ function isImportNode(node) { sourcemap = patchSourcemap(patches, sourcemap, smc); return { javascript, sourcemap, nlsKeys, nls }; } - nls_1.patch = patch; function patchFiles(javascriptFile, typescript) { + const ts = require('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); - const { javascript, sourcemap, nlsKeys, nls } = patch(moduleId, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap); + const { javascript, sourcemap, nlsKeys, nls } = patch(ts, moduleId, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap); const result = [fileFrom(javascriptFile, javascript)]; result[0].sourceMap = sourcemap; if (nlsKeys) { @@ -349,6 +344,5 @@ function isImportNode(node) { } return result; } - nls_1.patchFiles = patchFiles; -})(nls || (nls = {})); -module.exports = nls; + _nls.patchFiles = patchFiles; +})(_nls || (_nls = {})); diff --git a/build/lib/nls.ts b/build/lib/nls.ts index bdb6a8c311..f82784d85d 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import * as lazy from 'lazy.js'; import { duplex, through } from 'event-stream'; import * as File from 'vinyl'; @@ -21,7 +21,7 @@ enum CollectStepResult { NoAndRecurse } -function collect(node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { +function collect(ts: typeof import('typescript'), node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { const result: ts.Node[] = []; function loop(node: ts.Node) { @@ -65,7 +65,7 @@ define([], [${ wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; /** * Returns a stream containing the patched JavaScript and source maps. */ -function nls(): NodeJS.ReadWriteStream { +export function nls(): NodeJS.ReadWriteStream { const input = through(); const output = input.pipe(through(function (f: FileSourceMap) { if (!f.sourceMap) { @@ -87,48 +87,48 @@ function nls(): NodeJS.ReadWriteStream { return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); } - nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); + _nls.patchFiles(f, typescript).forEach(f => this.emit('data', f)); })); return duplex(input, output); } -function isImportNode(node: ts.Node): boolean { +function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } -module nls { +module _nls { - export interface INlsStringResult { + interface INlsStringResult { javascript: string; sourcemap: sm.RawSourceMap; nls?: string; nlsKeys?: string; } - export interface ISpan { + interface ISpan { start: ts.LineAndCharacter; end: ts.LineAndCharacter; } - export interface ILocalizeCall { + interface ILocalizeCall { keySpan: ISpan; key: string; valueSpan: ISpan; value: string; } - export interface ILocalizeAnalysisResult { + interface ILocalizeAnalysisResult { localizeCalls: ILocalizeCall[]; nlsExpressions: ISpan[]; } - export interface IPatch { + interface IPatch { span: ISpan; content: string; } - export function fileFrom(file: File, contents: string, path: string = file.path) { + function fileFrom(file: File, contents: string, path: string = file.path) { return new File({ contents: Buffer.from(contents), base: file.base, @@ -137,20 +137,20 @@ module nls { }); } - export function mappedPositionFrom(source: string, lc: ts.LineAndCharacter): sm.MappedPosition { + function mappedPositionFrom(source: string, lc: ts.LineAndCharacter): sm.MappedPosition { return { source, line: lc.line + 1, column: lc.character }; } - export function lcFrom(position: sm.Position): ts.LineAndCharacter { + function lcFrom(position: sm.Position): ts.LineAndCharacter { return { line: position.line - 1, character: position.column }; } - export class SingleFileServiceHost implements ts.LanguageServiceHost { + class SingleFileServiceHost implements ts.LanguageServiceHost { private file: ts.IScriptSnapshot; private lib: ts.IScriptSnapshot; - constructor(private options: ts.CompilerOptions, private filename: string, contents: string) { + constructor(ts: typeof import('typescript'), private options: ts.CompilerOptions, private filename: string, contents: string) { this.file = ts.ScriptSnapshot.fromString(contents); this.lib = ts.ScriptSnapshot.fromString(''); } @@ -163,7 +163,7 @@ module nls { getDefaultLibFileName = () => 'lib.d.ts'; } - function isCallExpressionWithinTextSpanCollectStep(textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { + function isCallExpressionWithinTextSpanCollectStep(ts: typeof import('typescript'), textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { return CollectStepResult.No; } @@ -171,14 +171,14 @@ module nls { return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; } - export function analyze(contents: string, options: ts.CompilerOptions = {}): ILocalizeAnalysisResult { + function analyze(ts: typeof import('typescript'), contents: string, options: ts.CompilerOptions = {}): ILocalizeAnalysisResult { const filename = 'file.ts'; - const serviceHost = new SingleFileServiceHost(Object.assign(clone(options), { noResolve: true }), filename, contents); + const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents); const service = ts.createLanguageService(serviceHost); const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); // all imports - const imports = lazy(collect(sourceFile, n => isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); + const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); // import nls = require('vs/nls'); const importEqualsDeclarations = imports @@ -215,7 +215,7 @@ module nls { .filter(r => !r.isWriteAccess) // find the deepest call expressions AST nodes that contain those references - .map(r => collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(r.textSpan, n))) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) .map(n => n) @@ -246,7 +246,7 @@ module nls { // find the deepest call expressions AST nodes that contain those references const localizeCallExpressions = localizeReferences .concat(namedLocalizeReferences) - .map(r => collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(r.textSpan, n))) + .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) .map(n => n); @@ -270,7 +270,7 @@ module nls { }; } - export class TextModel { + class TextModel { private lines: string[]; private lineEndings: string[]; @@ -336,8 +336,8 @@ module nls { } } - export function patchJavascript(patches: IPatch[], contents: string, moduleId: string): string { - const model = new nls.TextModel(contents); + function patchJavascript(patches: IPatch[], contents: string, moduleId: string): string { + const model = new TextModel(contents); // patch the localize calls lazy(patches).reverse().each(p => model.apply(p)); @@ -350,7 +350,7 @@ module nls { return model.toString(); } - export function patchSourcemap(patches: IPatch[], rsm: sm.RawSourceMap, smc: sm.SourceMapConsumer): sm.RawSourceMap { + function patchSourcemap(patches: IPatch[], rsm: sm.RawSourceMap, smc: sm.SourceMapConsumer): sm.RawSourceMap { const smg = new sm.SourceMapGenerator({ file: rsm.file, sourceRoot: rsm.sourceRoot @@ -395,8 +395,8 @@ module nls { return JSON.parse(smg.toString()); } - export function patch(moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult { - const { localizeCalls, nlsExpressions } = analyze(typescript); + function patch(ts: typeof import('typescript'), moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult { + const { localizeCalls, nlsExpressions } = analyze(ts, typescript); if (localizeCalls.length === 0) { return { javascript, sourcemap }; @@ -438,12 +438,14 @@ module nls { } export function patchFiles(javascriptFile: File, typescript: string): File[] { + const ts = require('typescript') as typeof import('typescript'); // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') .replace(/\\/g, '/'); const { javascript, sourcemap, nlsKeys, nls } = patch( + ts, moduleId, typescript, javascriptFile.contents.toString(), @@ -464,5 +466,3 @@ module nls { return result; } } - -export = nls; diff --git a/build/lib/node.js b/build/lib/node.js index 4e723015ca..5fec3c04d5 100644 --- a/build/lib/node.js +++ b/build/lib/node.js @@ -10,6 +10,8 @@ const root = path.dirname(path.dirname(__dirname)); const yarnrcPath = path.join(root, 'remote', '.yarnrc'); const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1]; -const node = process.platform === 'win32' ? 'node.exe' : 'node'; -const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); +const platform = process.platform; +const arch = platform === 'darwin' ? 'x64' : process.arch; +const node = platform === 'win32' ? 'node.exe' : 'node'; +const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); console.log(nodePath); diff --git a/build/lib/node.ts b/build/lib/node.ts index 651fa9eebc..5ef7f17995 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -10,7 +10,11 @@ const root = path.dirname(path.dirname(__dirname)); const yarnrcPath = path.join(root, 'remote', '.yarnrc'); const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)![1]; -const node = process.platform === 'win32' ? 'node.exe' : 'node'; -const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); -console.log(nodePath); \ No newline at end of file +const platform = process.platform; +const arch = platform === 'darwin' ? 'x64' : process.arch; + +const node = platform === 'win32' ? 'node.exe' : 'node'; +const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); + +console.log(nodePath); diff --git a/build/lib/optimize.js b/build/lib/optimize.js index cb3d292b19..fb68cea916 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -8,17 +8,11 @@ exports.minifyTask = exports.optimizeTask = exports.loaderConfig = void 0; const es = require("event-stream"); const gulp = require("gulp"); const concat = require("gulp-concat"); -const minifyCSS = require("gulp-cssnano"); const filter = require("gulp-filter"); -const flatmap = require("gulp-flatmap"); -const sourcemaps = require("gulp-sourcemaps"); -const uglify = require("gulp-uglify"); -const composer = require("gulp-uglify/composer"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const path = require("path"); const pump = require("pump"); -const terser = require("terser"); const VinylFile = require("vinyl"); const bundle = require("./bundle"); const i18n_1 = require("./i18n"); @@ -126,6 +120,7 @@ function optimizeTask(opts) { const out = opts.out; const fileContentMapper = opts.fileContentMapper || ((contents, _path) => contents); return function () { + const sourcemaps = require('gulp-sourcemaps'); const bundlesStream = es.through(); // this stream will contain the bundled files const resourcesStream = es.through(); // this stream will contain the resources const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json @@ -168,55 +163,32 @@ function optimizeTask(opts) { }; } exports.optimizeTask = optimizeTask; -/** - * Wrap around uglify and allow the preserveComments function - * to have a file "context" to include our copyright only once per file. - */ -function uglifyWithCopyrights() { - const preserveComments = (f) => { - return (_node, comment) => { - const text = comment.value; - const type = comment.type; - if (/@minifier_do_not_preserve/.test(text)) { - return false; - } - const isOurCopyright = IS_OUR_COPYRIGHT_REGEXP.test(text); - if (isOurCopyright) { - if (f.__hasOurCopyright) { - return false; - } - f.__hasOurCopyright = true; - return true; - } - if ('comment2' === type) { - // check for /*!. Note that text doesn't contain leading /* - return (text.length > 0 && text[0] === '!') || /@preserve|license|@cc_on|copyright/i.test(text); - } - else if ('comment1' === type) { - return /license|copyright/i.test(text); - } - return false; - }; - }; - const minify = composer(terser); - const input = es.through(); - const output = input - .pipe(flatmap((stream, f) => { - return stream.pipe(minify({ - output: { - comments: preserveComments(f), - max_line_len: 1024 - } - })); - })); - return es.duplex(input, output); -} function minifyTask(src, sourceMapBaseUrl) { + const esbuild = require('esbuild'); const sourceMappingURL = sourceMapBaseUrl ? ((f) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; return cb => { + const cssnano = require('cssnano'); + const postcss = require('gulp-postcss'); + const sourcemaps = require('gulp-sourcemaps'); const jsFilter = filter('**/*.js', { restore: true }); const cssFilter = filter('**/*.css', { restore: true }); - pump(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), uglifyWithCopyrights(), jsFilter.restore, cssFilter, minifyCSS({ reduceIdents: false }), cssFilter.restore, sourcemaps.mapSources((sourcePath) => { + pump(gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), es.map((f, cb) => { + esbuild.build({ + entryPoints: [f.path], + minify: true, + sourcemap: 'external', + outdir: '.', + platform: 'node', + target: ['node12.18'], + write: false + }).then(res => { + const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path)); + const sourceMapFile = res.outputFiles.find(f => /\.js\.map$/.test(f.path)); + f.contents = Buffer.from(jsFile.contents); + f.sourceMap = JSON.parse(sourceMapFile.text); + cb(undefined, f); + }, cb); + }), jsFilter.restore, cssFilter, postcss([cssnano({ preset: 'default' })]), cssFilter.restore, sourcemaps.mapSources((sourcePath) => { if (sourcePath === 'bootstrap-fork.js') { return 'bootstrap-fork.orig.js'; } @@ -226,12 +198,7 @@ function minifyTask(src, sourceMapBaseUrl) { sourceRoot: undefined, includeContent: true, addComment: true - }), gulp.dest(src + '-min'), (err) => { - if (err instanceof uglify.GulpUglifyError) { - console.error(`Uglify error in '${err.cause && err.cause.filename}'`); - } - cb(err); - }); + }), gulp.dest(src + '-min'), (err) => cb(err)); }; } exports.minifyTask = minifyTask; diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 6992cb88f5..aa04891d17 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -8,17 +8,11 @@ import * as es from 'event-stream'; import * as gulp from 'gulp'; import * as concat from 'gulp-concat'; -import * as minifyCSS from 'gulp-cssnano'; import * as filter from 'gulp-filter'; -import * as flatmap from 'gulp-flatmap'; -import * as sourcemaps from 'gulp-sourcemaps'; -import * as uglify from 'gulp-uglify'; -import * as composer from 'gulp-uglify/composer'; import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import * as path from 'path'; import * as pump from 'pump'; -import * as terser from 'terser'; import * as VinylFile from 'vinyl'; import * as bundle from './bundle'; import { Language, processNlsFiles } from './i18n'; @@ -186,6 +180,8 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr const fileContentMapper = opts.fileContentMapper || ((contents: string, _path: string) => contents); return function () { + const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + const bundlesStream = es.through(); // this stream will contain the bundled files const resourcesStream = es.through(); // this stream will contain the resources const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json @@ -237,62 +233,15 @@ export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStr }; } -declare class FileWithCopyright extends VinylFile { - public __hasOurCopyright: boolean; -} -/** - * Wrap around uglify and allow the preserveComments function - * to have a file "context" to include our copyright only once per file. - */ -function uglifyWithCopyrights(): NodeJS.ReadWriteStream { - const preserveComments = (f: FileWithCopyright) => { - return (_node: any, comment: { value: string; type: string; }) => { - const text = comment.value; - const type = comment.type; - - if (/@minifier_do_not_preserve/.test(text)) { - return false; - } - - const isOurCopyright = IS_OUR_COPYRIGHT_REGEXP.test(text); - - if (isOurCopyright) { - if (f.__hasOurCopyright) { - return false; - } - f.__hasOurCopyright = true; - return true; - } - - if ('comment2' === type) { - // check for /*!. Note that text doesn't contain leading /* - return (text.length > 0 && text[0] === '!') || /@preserve|license|@cc_on|copyright/i.test(text); - } else if ('comment1' === type) { - return /license|copyright/i.test(text); - } - return false; - }; - }; - - const minify = (composer as any)(terser); - const input = es.through(); - const output = input - .pipe(flatmap((stream, f) => { - return stream.pipe(minify({ - output: { - comments: preserveComments(f), - max_line_len: 1024 - } - })); - })); - - return es.duplex(input, output); -} - export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => void { + const esbuild = require('esbuild') as typeof import('esbuild'); const sourceMappingURL = sourceMapBaseUrl ? ((f: any) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; return cb => { + const cssnano = require('cssnano') as typeof import('cssnano'); + const postcss = require('gulp-postcss') as typeof import('gulp-postcss'); + const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + const jsFilter = filter('**/*.js', { restore: true }); const cssFilter = filter('**/*.css', { restore: true }); @@ -300,10 +249,28 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => gulp.src([src + '/**', '!' + src + '/**/*.map']), jsFilter, sourcemaps.init({ loadMaps: true }), - uglifyWithCopyrights(), + es.map((f: any, cb) => { + esbuild.build({ + entryPoints: [f.path], + minify: true, + sourcemap: 'external', + outdir: '.', + platform: 'node', + target: ['node12.18'], + write: false + }).then(res => { + const jsFile = res.outputFiles.find(f => /\.js$/.test(f.path))!; + const sourceMapFile = res.outputFiles.find(f => /\.js\.map$/.test(f.path))!; + + f.contents = Buffer.from(jsFile.contents); + f.sourceMap = JSON.parse(sourceMapFile.text); + + cb(undefined, f); + }, cb); + }), jsFilter.restore, cssFilter, - minifyCSS({ reduceIdents: false }), + postcss([cssnano({ preset: 'default' })]), cssFilter.restore, (sourcemaps).mapSources((sourcePath: string) => { if (sourcePath === 'bootstrap-fork.js') { @@ -318,13 +285,7 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => includeContent: true, addComment: true } as any), - gulp.dest(src + '-min') - , (err: any) => { - if (err instanceof (uglify as any).GulpUglifyError) { - console.error(`Uglify error in '${err.cause && err.cause.filename}'`); - } - - cb(err); - }); + gulp.dest(src + '-min'), + (err: any) => cb(err)); }; } diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 41e1ac08e8..42d3d3aa39 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -5,7 +5,6 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.createESMSourcesAndResources2 = exports.extractEditor = void 0; -const ts = require("typescript"); const fs = require("fs"); const path = require("path"); const tss = require("./treeshaking"); @@ -29,6 +28,7 @@ function writeFile(filePath, contents) { } function extractEditor(options) { var _a; + const ts = require('typescript'); const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions; if (tsConfig.extends) { @@ -115,6 +115,7 @@ function extractEditor(options) { } exports.extractEditor = extractEditor; function createESMSourcesAndResources2(options) { + const ts = require('typescript'); const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 2bef371011..d55cdd5497 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; import * as tss from './treeshaking'; @@ -31,6 +30,8 @@ function writeFile(filePath: string, contents: Buffer | string): void { } export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string }): void { + const ts = require('typescript') as typeof import('typescript'); + const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions: { [key: string]: any }; if (tsConfig.extends) { @@ -134,6 +135,8 @@ export interface IOptions2 { } export function createESMSourcesAndResources2(options: IOptions2): void { + const ts = require('typescript') as typeof import('typescript'); + const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); diff --git a/build/lib/stats.js b/build/lib/stats.js index cf60b69125..f98452e1d8 100644 --- a/build/lib/stats.js +++ b/build/lib/stats.js @@ -8,7 +8,6 @@ exports.submitAllStats = exports.createStatsStream = void 0; const es = require("event-stream"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); -const appInsights = require("applicationinsights"); class Entry { constructor(name, totalCount, totalSize) { this.name = name; @@ -73,6 +72,7 @@ function createStatsStream(group, log) { } exports.createStatsStream = createStatsStream; function submitAllStats(productJson, commit) { + const appInsights = require('applicationinsights'); const sorted = []; // move entries for single files to the front _entries.forEach(value => { diff --git a/build/lib/stats.ts b/build/lib/stats.ts index 9d61de3811..92e2724b27 100644 --- a/build/lib/stats.ts +++ b/build/lib/stats.ts @@ -9,7 +9,6 @@ import * as es from 'event-stream'; import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import * as File from 'vinyl'; -import * as appInsights from 'applicationinsights'; class Entry { constructor(readonly name: string, public totalCount: number, public totalSize: number) { } @@ -75,6 +74,7 @@ export function createStatsStream(group: string, log?: boolean): es.ThroughStrea } export function submitAllStats(productJson: any, commit: string): Promise { + const appInsights = require('applicationinsights') as typeof import('applicationinsights'); const sorted: Entry[] = []; // move entries for single files to the front diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index a9ac363113..d7cd2724fe 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -7,7 +7,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.shake = exports.toStringShakeLevel = exports.ShakeLevel = void 0; const fs = require("fs"); const path = require("path"); -const ts = require("typescript"); const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); var ShakeLevel; (function (ShakeLevel) { @@ -41,7 +40,8 @@ function printDiagnostics(options, diagnostics) { } } function shake(options) { - const languageService = createTypeScriptLanguageService(options); + const ts = require('typescript'); + const languageService = createTypeScriptLanguageService(ts, options); const program = languageService.getProgram(); const globalDiagnostics = program.getGlobalDiagnostics(); if (globalDiagnostics.length > 0) { @@ -58,14 +58,14 @@ function shake(options) { printDiagnostics(options, semanticDiagnostics); throw new Error(`Compilation Errors encountered.`); } - markNodes(languageService, options); - return generateResult(languageService, options.shakeLevel); + markNodes(ts, languageService, options); + return generateResult(ts, languageService, options.shakeLevel); } exports.shake = shake; //#region Discovery, LanguageService & Setup -function createTypeScriptLanguageService(options) { +function createTypeScriptLanguageService(ts, options) { // Discover referenced files - const FILES = discoverAndReadFiles(options); + const FILES = discoverAndReadFiles(ts, options); // Add fake usage files options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { FILES[`inlineEntryPoint.${index}.ts`] = inlineEntryPoint; @@ -76,15 +76,15 @@ function createTypeScriptLanguageService(options) { FILES[typing] = fs.readFileSync(filePath).toString(); }); // Resolve libs - const RESOLVED_LIBS = processLibFiles(options); + const RESOLVED_LIBS = processLibFiles(ts, options); const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, options.sourcesRoot).options; - const host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, compilerOptions); + const host = new TypeScriptLanguageServiceHost(ts, RESOLVED_LIBS, FILES, compilerOptions); return ts.createLanguageService(host); } /** * Read imports and follow them until all files have been handled */ -function discoverAndReadFiles(options) { +function discoverAndReadFiles(ts, options) { const FILES = {}; const in_queue = Object.create(null); const queue = []; @@ -137,7 +137,7 @@ function discoverAndReadFiles(options) { /** * Read lib files and follow lib references */ -function processLibFiles(options) { +function processLibFiles(ts, options) { const stack = [...options.compilerOptions.lib]; const result = {}; while (stack.length > 0) { @@ -161,7 +161,8 @@ function processLibFiles(options) { * A TypeScript language service host */ class TypeScriptLanguageServiceHost { - constructor(libs, files, compilerOptions) { + constructor(ts, libs, files, compilerOptions) { + this._ts = ts; this._libs = libs; this._files = files; this._compilerOptions = compilerOptions; @@ -183,17 +184,17 @@ class TypeScriptLanguageServiceHost { } getScriptSnapshot(fileName) { if (this._files.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._files[fileName]); + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); } else if (this._libs.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._libs[fileName]); + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); } else { - return ts.ScriptSnapshot.fromString(''); + return this._ts.ScriptSnapshot.fromString(''); } } getScriptKind(_fileName) { - return ts.ScriptKind.TS; + return this._ts.ScriptKind.TS; } getCurrentDirectory() { return ''; @@ -240,7 +241,7 @@ function nodeOrChildIsBlack(node) { } return false; } -function markNodes(languageService, options) { +function markNodes(ts, languageService, options) { const program = languageService.getProgram(); if (!program) { throw new Error('Could not get program from language service'); @@ -342,7 +343,7 @@ function markNodes(languageService, options) { if (!referenceSourceFile) { continue; } - const referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + const referenceNode = getTokenAtPosition(ts, referenceSourceFile, reference.textSpan.start, false, false); if (ts.isMethodDeclaration(referenceNode.parent) || ts.isPropertyDeclaration(referenceNode.parent) || ts.isGetAccessor(referenceNode.parent) @@ -408,7 +409,7 @@ function markNodes(languageService, options) { } const nodeSourceFile = node.getSourceFile(); const loop = (node) => { - const [symbol, symbolImportNode] = getRealNodeSymbol(checker, node); + const [symbol, symbolImportNode] = getRealNodeSymbol(ts, checker, node); if (symbolImportNode) { setColor(symbolImportNode, 2 /* Black */); } @@ -420,7 +421,7 @@ function markNodes(languageService, options) { // (they can be the declaration of a module import) continue; } - if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { enqueue_black(declaration.name); for (let j = 0; j < declaration.members.length; j++) { const member = declaration.members[j]; @@ -484,7 +485,7 @@ function nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol) { } return false; } -function generateResult(languageService, shakeLevel) { +function generateResult(ts, languageService, shakeLevel) { const program = languageService.getProgram(); if (!program) { throw new Error('Could not get program from language service'); @@ -614,11 +615,11 @@ function generateResult(languageService, shakeLevel) { } //#endregion //#region Utils -function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration) { +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration) { if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { for (const heritageClause of declaration.heritageClauses) { for (const type of heritageClause.types) { - const symbol = findSymbolFromHeritageType(checker, type); + const symbol = findSymbolFromHeritageType(ts, checker, type); if (symbol) { const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { @@ -630,22 +631,22 @@ function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, } return false; } -function findSymbolFromHeritageType(checker, type) { +function findSymbolFromHeritageType(ts, checker, type) { if (ts.isExpressionWithTypeArguments(type)) { - return findSymbolFromHeritageType(checker, type.expression); + return findSymbolFromHeritageType(ts, checker, type.expression); } if (ts.isIdentifier(type)) { - return getRealNodeSymbol(checker, type)[0]; + return getRealNodeSymbol(ts, checker, type)[0]; } if (ts.isPropertyAccessExpression(type)) { - return findSymbolFromHeritageType(checker, type.name); + return findSymbolFromHeritageType(ts, checker, type.name); } return null; } /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ -function getRealNodeSymbol(checker, node) { +function getRealNodeSymbol(ts, checker, node) { const getPropertySymbolsFromContextualType = ts.getPropertySymbolsFromContextualType; const getContainingObjectLiteralElement = ts.getContainingObjectLiteralElement; const getNameFromPropertyName = ts.getNameFromPropertyName; @@ -758,7 +759,7 @@ function getRealNodeSymbol(checker, node) { return [null, null]; } /** Get the token whose text contains the position */ -function getTokenAtPosition(sourceFile, position, allowPositionInLeadingTrivia, includeEndPosition) { +function getTokenAtPosition(ts, sourceFile, position, allowPositionInLeadingTrivia, includeEndPosition) { let current = sourceFile; outer: while (true) { // find the child that contains 'position' diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 495541a898..78ff4d3b8a 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; const TYPESCRIPT_LIB_FOLDER = path.dirname(require.resolve('typescript/lib/lib.d.ts')); @@ -82,7 +82,8 @@ function printDiagnostics(options: ITreeShakingOptions, diagnostics: ReadonlyArr } export function shake(options: ITreeShakingOptions): ITreeShakingResult { - const languageService = createTypeScriptLanguageService(options); + const ts = require('typescript') as typeof import('typescript'); + const languageService = createTypeScriptLanguageService(ts, options); const program = languageService.getProgram()!; const globalDiagnostics = program.getGlobalDiagnostics(); @@ -103,15 +104,15 @@ export function shake(options: ITreeShakingOptions): ITreeShakingResult { throw new Error(`Compilation Errors encountered.`); } - markNodes(languageService, options); + markNodes(ts, languageService, options); - return generateResult(languageService, options.shakeLevel); + return generateResult(ts, languageService, options.shakeLevel); } //#region Discovery, LanguageService & Setup -function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.LanguageService { +function createTypeScriptLanguageService(ts: typeof import('typescript'), options: ITreeShakingOptions): ts.LanguageService { // Discover referenced files - const FILES = discoverAndReadFiles(options); + const FILES = discoverAndReadFiles(ts, options); // Add fake usage files options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { @@ -125,18 +126,18 @@ function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.Langu }); // Resolve libs - const RESOLVED_LIBS = processLibFiles(options); + const RESOLVED_LIBS = processLibFiles(ts, options); const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, options.sourcesRoot).options; - const host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, compilerOptions); + const host = new TypeScriptLanguageServiceHost(ts, RESOLVED_LIBS, FILES, compilerOptions); return ts.createLanguageService(host); } /** * Read imports and follow them until all files have been handled */ -function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { +function discoverAndReadFiles(ts: typeof import('typescript'), options: ITreeShakingOptions): IFileMap { const FILES: IFileMap = {}; const in_queue: { [module: string]: boolean; } = Object.create(null); @@ -199,7 +200,7 @@ function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { /** * Read lib files and follow lib references */ -function processLibFiles(options: ITreeShakingOptions): ILibMap { +function processLibFiles(ts: typeof import('typescript'), options: ITreeShakingOptions): ILibMap { const stack: string[] = [...options.compilerOptions.lib]; const result: ILibMap = {}; @@ -232,11 +233,13 @@ interface IFileMap { [fileName: string]: string; } */ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + private readonly _ts: typeof import('typescript'); private readonly _libs: ILibMap; private readonly _files: IFileMap; private readonly _compilerOptions: ts.CompilerOptions; - constructor(libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + constructor(ts: typeof import('typescript'), libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) { + this._ts = ts; this._libs = libs; this._files = files; this._compilerOptions = compilerOptions; @@ -262,15 +265,15 @@ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { } getScriptSnapshot(fileName: string): ts.IScriptSnapshot { if (this._files.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._files[fileName]); + return this._ts.ScriptSnapshot.fromString(this._files[fileName]); } else if (this._libs.hasOwnProperty(fileName)) { - return ts.ScriptSnapshot.fromString(this._libs[fileName]); + return this._ts.ScriptSnapshot.fromString(this._libs[fileName]); } else { - return ts.ScriptSnapshot.fromString(''); + return this._ts.ScriptSnapshot.fromString(''); } } getScriptKind(_fileName: string): ts.ScriptKind { - return ts.ScriptKind.TS; + return this._ts.ScriptKind.TS; } getCurrentDirectory(): string { return ''; @@ -320,7 +323,7 @@ function nodeOrChildIsBlack(node: ts.Node): boolean { return false; } -function markNodes(languageService: ts.LanguageService, options: ITreeShakingOptions) { +function markNodes(ts: typeof import('typescript'), languageService: ts.LanguageService, options: ITreeShakingOptions) { const program = languageService.getProgram(); if (!program) { throw new Error('Could not get program from language service'); @@ -446,7 +449,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt continue; } - const referenceNode = getTokenAtPosition(referenceSourceFile, reference.textSpan.start, false, false); + const referenceNode = getTokenAtPosition(ts, referenceSourceFile, reference.textSpan.start, false, false); if ( ts.isMethodDeclaration(referenceNode.parent) || ts.isPropertyDeclaration(referenceNode.parent) @@ -522,7 +525,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt const nodeSourceFile = node.getSourceFile(); const loop = (node: ts.Node) => { - const [symbol, symbolImportNode] = getRealNodeSymbol(checker, node); + const [symbol, symbolImportNode] = getRealNodeSymbol(ts, checker, node); if (symbolImportNode) { setColor(symbolImportNode, NodeColor.Black); } @@ -536,7 +539,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt continue; } - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { enqueue_black(declaration.name!); for (let j = 0; j < declaration.members.length; j++) { @@ -607,7 +610,7 @@ function nodeIsInItsOwnDeclaration(nodeSourceFile: ts.SourceFile, node: ts.Node, return false; } -function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult { +function generateResult(ts: typeof import('typescript'), languageService: ts.LanguageService, shakeLevel: ShakeLevel): ITreeShakingResult { const program = languageService.getProgram(); if (!program) { throw new Error('Could not get program from language service'); @@ -752,11 +755,11 @@ function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLe //#region Utils -function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean { +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts: typeof import('typescript'), program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean { if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { for (const heritageClause of declaration.heritageClauses) { for (const type of heritageClause.types) { - const symbol = findSymbolFromHeritageType(checker, type); + const symbol = findSymbolFromHeritageType(ts, checker, type); if (symbol) { const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { @@ -769,15 +772,15 @@ function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program: ts.Progra return false; } -function findSymbolFromHeritageType(checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null { +function findSymbolFromHeritageType(ts: typeof import('typescript'), checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null { if (ts.isExpressionWithTypeArguments(type)) { - return findSymbolFromHeritageType(checker, type.expression); + return findSymbolFromHeritageType(ts, checker, type.expression); } if (ts.isIdentifier(type)) { - return getRealNodeSymbol(checker, type)[0]; + return getRealNodeSymbol(ts, checker, type)[0]; } if (ts.isPropertyAccessExpression(type)) { - return findSymbolFromHeritageType(checker, type.name); + return findSymbolFromHeritageType(ts, checker, type.name); } return null; } @@ -785,7 +788,7 @@ function findSymbolFromHeritageType(checker: ts.TypeChecker, type: ts.Expression /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ -function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | null, ts.Declaration | null] { +function getRealNodeSymbol(ts: typeof import('typescript'), checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | null, ts.Declaration | null] { // Use some TypeScript internals to avoid code duplication type ObjectLiteralElementWithName = ts.ObjectLiteralElement & { name: ts.PropertyName; parent: ts.ObjectLiteralExpression | ts.JsxAttributes }; @@ -913,7 +916,7 @@ function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | } /** Get the token whose text contains the position */ -function getTokenAtPosition(sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node { +function getTokenAtPosition(ts: typeof import('typescript'), sourceFile: ts.SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeEndPosition: boolean): ts.Node { let current: ts.Node = sourceFile; outer: while (true) { // find the child that contains 'position' diff --git a/build/lib/watch/index.js b/build/lib/watch/index.js index d07d6efdde..6d4456a74e 100644 --- a/build/lib/watch/index.js +++ b/build/lib/watch/index.js @@ -1,10 +1,9 @@ +"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); - module.exports = function () { - return watch.apply(null, arguments); + return watch.apply(null, arguments); }; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-animations.css b/build/lib/watch/index.ts similarity index 64% rename from src/vs/base/browser/ui/codicons/codicon/codicon-animations.css rename to build/lib/watch/index.ts index 2f15865606..d07d6efdde 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon-animations.css +++ b/build/lib/watch/index.ts @@ -3,13 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -@keyframes codicon-spin { - 100% { - transform:rotate(360deg); - } -} +const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); -.codicon-animation-spin { - /* Use steps to throttle FPS to reduce CPU usage */ - animation: codicon-spin 1.5s steps(30) infinite; -} +module.exports = function () { + return watch.apply(null, arguments); +}; diff --git a/build/lib/watch/package.json b/build/lib/watch/package.json index d509f873e8..e2e4f55202 100644 --- a/build/lib/watch/package.json +++ b/build/lib/watch/package.json @@ -7,6 +7,6 @@ "license": "MIT", "devDependencies": {}, "dependencies": { - "vscode-gulp-watch": "^5.0.2" + "vscode-gulp-watch": "^5.0.3" } } diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js index acaa50b8c3..f63e48dd91 100644 --- a/build/lib/watch/watch-win32.js +++ b/build/lib/watch/watch-win32.js @@ -1,107 +1,100 @@ +"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -var path = require('path'); -var cp = require('child_process'); -var fs = require('fs'); -var File = require('vinyl'); -var es = require('event-stream'); -var filter = require('gulp-filter'); - -var watcherPath = path.join(__dirname, 'watcher.exe'); - +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const cp = require("child_process"); +const fs = require("fs"); +const File = require("vinyl"); +const es = require("event-stream"); +const filter = require("gulp-filter"); +const watcherPath = path.join(__dirname, 'watcher.exe'); function toChangeType(type) { - switch (type) { - case '0': return 'change'; - case '1': return 'add'; - default: return 'unlink'; - } + switch (type) { + case '0': return 'change'; + case '1': return 'add'; + default: return 'unlink'; + } } - function watch(root) { - var result = es.through(); - var child = cp.spawn(watcherPath, [root]); - - child.stdout.on('data', function (data) { - var lines = data.toString('utf8').split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i].trim(); - if (line.length === 0) { - continue; - } - - var changeType = line[0]; - var changePath = line.substr(2); - - // filter as early as possible - if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { - continue; - } - - var changePathFull = path.join(root, changePath); - - var file = new File({ - path: changePathFull, - base: root - }); - file.event = toChangeType(changeType); - result.emit('data', file); - } - }); - - child.stderr.on('data', function (data) { - result.emit('error', data); - }); - - child.on('exit', function (code) { - result.emit('error', 'Watcher died with code ' + code); - child = null; - }); - - process.once('SIGTERM', function () { process.exit(0); }); - process.once('SIGTERM', function () { process.exit(0); }); - process.once('exit', function () { child && child.kill(); }); - - return result; + const result = es.through(); + let child = cp.spawn(watcherPath, [root]); + child.stdout.on('data', function (data) { + const lines = data.toString('utf8').split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.length === 0) { + continue; + } + const changeType = line[0]; + const changePath = line.substr(2); + // filter as early as possible + if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { + continue; + } + const changePathFull = path.join(root, changePath); + const file = new File({ + path: changePathFull, + base: root + }); + file.event = toChangeType(changeType); + result.emit('data', file); + } + }); + child.stderr.on('data', function (data) { + result.emit('error', data); + }); + child.on('exit', function (code) { + result.emit('error', 'Watcher died with code ' + code); + child = null; + }); + process.once('SIGTERM', function () { process.exit(0); }); + process.once('SIGTERM', function () { process.exit(0); }); + process.once('exit', function () { if (child) { + child.kill(); + } }); + return result; } - -var cache = Object.create(null); - +const cache = Object.create(null); module.exports = function (pattern, options) { - options = options || {}; - - var cwd = path.normalize(options.cwd || process.cwd()); - var watcher = cache[cwd]; - - if (!watcher) { - watcher = cache[cwd] = watch(cwd); - } - - var rebase = !options.base ? es.through() : es.mapSync(function (f) { - f.base = options.base; - return f; - }); - - return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) - .pipe(es.map(function (file, cb) { - fs.stat(file.path, function (err, stat) { - if (err && err.code === 'ENOENT') { return cb(null, file); } - if (err) { return cb(); } - if (!stat.isFile()) { return cb(); } - - fs.readFile(file.path, function (err, contents) { - if (err && err.code === 'ENOENT') { return cb(null, file); } - if (err) { return cb(); } - - file.contents = contents; - file.stat = stat; - cb(null, file); - }); - }); - })) - .pipe(rebase); + options = options || {}; + const cwd = path.normalize(options.cwd || process.cwd()); + let watcher = cache[cwd]; + if (!watcher) { + watcher = cache[cwd] = watch(cwd); + } + const rebase = !options.base ? es.through() : es.mapSync(function (f) { + f.base = options.base; + return f; + }); + return watcher + .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git + .pipe(filter(pattern)) + .pipe(es.map(function (file, cb) { + fs.stat(file.path, function (err, stat) { + if (err && err.code === 'ENOENT') { + return cb(undefined, file); + } + if (err) { + return cb(); + } + if (!stat.isFile()) { + return cb(); + } + fs.readFile(file.path, function (err, contents) { + if (err && err.code === 'ENOENT') { + return cb(undefined, file); + } + if (err) { + return cb(); + } + file.contents = contents; + file.stat = stat; + cb(undefined, file); + }); + }); + })) + .pipe(rebase); }; diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts new file mode 100644 index 0000000000..e4f9719b39 --- /dev/null +++ b/build/lib/watch/watch-win32.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as File from 'vinyl'; +import * as es from 'event-stream'; +import * as filter from 'gulp-filter'; +import { Stream } from 'stream'; + +const watcherPath = path.join(__dirname, 'watcher.exe'); + +function toChangeType(type: '0' | '1' | '2'): 'change' | 'add' | 'unlink' { + switch (type) { + case '0': return 'change'; + case '1': return 'add'; + default: return 'unlink'; + } +} + +function watch(root: string): Stream { + const result = es.through(); + let child: cp.ChildProcess | null = cp.spawn(watcherPath, [root]); + + child.stdout!.on('data', function (data) { + const lines: string[] = data.toString('utf8').split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.length === 0) { + continue; + } + + const changeType = <'0' | '1' | '2'>line[0]; + const changePath = line.substr(2); + + // filter as early as possible + if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { + continue; + } + + const changePathFull = path.join(root, changePath); + + const file = new File({ + path: changePathFull, + base: root + }); + (file).event = toChangeType(changeType); + result.emit('data', file); + } + }); + + child.stderr!.on('data', function (data) { + result.emit('error', data); + }); + + child.on('exit', function (code) { + result.emit('error', 'Watcher died with code ' + code); + child = null; + }); + + process.once('SIGTERM', function () { process.exit(0); }); + process.once('SIGTERM', function () { process.exit(0); }); + process.once('exit', function () { if (child) { child.kill(); } }); + + return result; +} + +const cache: { [cwd: string]: Stream; } = Object.create(null); + +module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string; }) { + options = options || {}; + + const cwd = path.normalize(options.cwd || process.cwd()); + let watcher = cache[cwd]; + + if (!watcher) { + watcher = cache[cwd] = watch(cwd); + } + + const rebase = !options.base ? es.through() : es.mapSync(function (f: File) { + f.base = options!.base!; + return f; + }); + + return watcher + .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git + .pipe(filter(pattern)) + .pipe(es.map(function (file: File, cb) { + fs.stat(file.path, function (err, stat) { + if (err && err.code === 'ENOENT') { return cb(undefined, file); } + if (err) { return cb(); } + if (!stat.isFile()) { return cb(); } + + fs.readFile(file.path, function (err, contents) { + if (err && err.code === 'ENOENT') { return cb(undefined, file); } + if (err) { return cb(); } + + file.contents = contents; + file.stat = stat; + cb(undefined, file); + }); + }); + })) + .pipe(rebase); +}; diff --git a/build/lib/watch/yarn.lock b/build/lib/watch/yarn.lock index edbfe1f312..b0d7dd4a9f 100644 --- a/build/lib/watch/yarn.lock +++ b/build/lib/watch/yarn.lock @@ -2,7 +2,12 @@ # yarn lockfile v1 -ansi-colors@1.1.0, ansi-colors@^1.0.1: +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== @@ -21,15 +26,7 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - -anymatch@~3.1.1: +anymatch@^3.1.1, anymatch@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== @@ -37,33 +34,16 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= - dependencies: - arr-flatten "^1.0.1" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -arr-flatten@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -74,15 +54,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -90,10 +61,10 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== +chokidar@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -101,30 +72,20 @@ chokidar@3.3.0: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.2.0" + readdirp "~3.5.0" optionalDependencies: - fsevents "~2.1.1" + fsevents "~2.3.1" clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= -clone-stats@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" - integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= - clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= -clone@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - clone@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" @@ -149,20 +110,6 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= - dependencies: - is-posix-bracket "^0.1.0" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= - dependencies: - fill-range "^2.1.0" - extend-shallow@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" @@ -171,38 +118,16 @@ extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= - dependencies: - is-extglob "^1.0.0" - -fancy-log@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1" - integrity sha1-9BEl49hPLn2JpD0G2VjI94vha+E= +fancy-log@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== dependencies: ansi-gray "^0.1.1" color-support "^1.1.3" + parse-node-version "^1.0.0" time-stamp "^1.0.0" -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -217,47 +142,12 @@ first-chunk-stream@^2.0.0: dependencies: readable-stream "^2.0.2" -for-in@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +fsevents@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" + integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - -fsevents@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" - integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= - dependencies: - is-glob "^2.0.0" - -glob-parent@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.0: +glob-parent@^5.1.1, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -269,7 +159,7 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -inherits@^2.0.1, inherits@~2.0.3: +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -281,28 +171,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - is-extendable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" @@ -310,30 +178,11 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= - -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= - dependencies: - is-extglob "^1.0.0" - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -341,18 +190,6 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -365,120 +202,37 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= - -is-utf8@^0.2.0: +is-utf8@^0.2.0, is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= -isarray@1.0.0, isarray@~1.0.0: +isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -kind-of@^3.0.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - -micromatch@^2.1.5: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -normalize-path@^2.0.0, normalize-path@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -object-assign@^4.1.0: +object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-is-absolute@^1.0.1: +parse-node-version@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== -picomatch@^2.0.4: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -488,18 +242,6 @@ pify@^2.3.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - plugin-error@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -510,26 +252,12 @@ plugin-error@1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= - process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - -readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5: +readable-stream@^2.0.2, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -542,40 +270,27 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: - picomatch "^2.0.4" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== dependencies: - is-equal-shallow "^0.1.3" + picomatch "^2.2.1" remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= - replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" @@ -586,6 +301,18 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -593,6 +320,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +strip-bom-buf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz#1cb45aaf57530f4caf86c7f75179d2c9a51dd572" + integrity sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI= + dependencies: + is-utf8 "^0.2.1" + strip-bom-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" @@ -620,36 +354,26 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -vinyl-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" - integrity sha1-p+v1/779obfRjRQPyweyI++2dRo= +vinyl-file@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-3.0.0.tgz#b104d9e4409ffa325faadd520642d0a3b488b365" + integrity sha1-sQTZ5ECf+jJfqt1SBkLQo7SIs2U= dependencies: graceful-fs "^4.1.2" pify "^2.3.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" + strip-bom-buf "^1.0.0" strip-bom-stream "^2.0.0" - vinyl "^1.1.0" + vinyl "^2.0.1" -vinyl@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" - integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" - integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== +vinyl@^2.0.1, vinyl@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" + integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== dependencies: clone "^2.1.1" clone-buffer "^1.0.0" @@ -658,20 +382,19 @@ vinyl@^2.1.0: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" -vscode-gulp-watch@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/vscode-gulp-watch/-/vscode-gulp-watch-5.0.2.tgz#0060ba8d091284a6fbd7e608aa318a9c1d73b840" - integrity sha512-l2v+W3iQvxpX2ny2C7eJTd+83rQXiZ85KGY0mub/QRqUxgDc+KH/EYiw4mttzIhPzVBmxrUO4RcLNbPdccg0mQ== +vscode-gulp-watch@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vscode-gulp-watch/-/vscode-gulp-watch-5.0.3.tgz#1ca1c03581d43692ecb1fe0b9afd4256faeb701b" + integrity sha512-MTUp2yLE9CshhkNSNV58EQNxQSeF8lIj3mkXZX9a1vAk+EQNM2PAYdPUDSd/P/08W3PMHGznEiZyfK7JAjLosg== dependencies: - ansi-colors "1.1.0" - anymatch "^1.3.0" - chokidar "3.3.0" - fancy-log "1.3.2" - glob-parent "^3.0.1" + ansi-colors "4.1.1" + anymatch "^3.1.1" + chokidar "3.5.1" + fancy-log "^1.3.3" + glob-parent "^5.1.1" normalize-path "^3.0.0" - object-assign "^4.1.0" - path-is-absolute "^1.0.1" + object-assign "^4.1.1" plugin-error "1.0.1" - readable-stream "^2.2.2" - vinyl "^2.1.0" - vinyl-file "^2.0.0" + readable-stream "^3.6.0" + vinyl "^2.2.0" + vinyl-file "^3.0.0" diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index e7129d5ec0..195b4efc2b 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -10,6 +10,7 @@ declare namespace monaco { export type Thenable = PromiseLike; export interface Environment { + globalAPI?: boolean; baseUrl?: string; getWorker?(workerId: string, label: string): Worker; getWorkerUrl?(workerId: string, label: string): string; diff --git a/build/monaco/package.json b/build/monaco/package.json index 70021689eb..5c764ed0fa 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -1,7 +1,7 @@ { "name": "monaco-editor-core", "private": true, - "version": "0.19.2", + "version": "0.22.0", "description": "A browser based code editor", "author": "Microsoft Corporation", "license": "MIT", diff --git a/build/npm/dirs.js b/build/npm/dirs.js new file mode 100644 index 0000000000..6257125733 --- /dev/null +++ b/build/npm/dirs.js @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Complete list of directories where yarn should be executed to install node modules +exports.dirs = [ + '', + 'build', + 'build/lib/watch', + 'extensions', + 'extensions/configuration-editing', + 'extensions/css-language-features', + 'extensions/css-language-features/server', + 'extensions/debug-auto-launch', + 'extensions/debug-server-ready', + 'extensions/emmet', + 'extensions/extension-editing', + 'extensions/git', + 'extensions/git-ui', + 'extensions/github', + 'extensions/github-authentication', + 'extensions/grunt', + 'extensions/gulp', + 'extensions/html-language-features', + 'extensions/html-language-features/server', + 'extensions/image-preview', + 'extensions/jake', + 'extensions/json-language-features', + 'extensions/json-language-features/server', + 'extensions/markdown-language-features', + 'extensions/merge-conflict', + 'extensions/microsoft-authentication', + 'extensions/npm', + 'extensions/php-language-features', + 'extensions/search-result', + 'extensions/simple-browser', + 'extensions/testing-editor-contributions', + 'extensions/typescript-language-features', + 'extensions/vscode-api-tests', + 'extensions/vscode-colorize-tests', + 'extensions/vscode-custom-editor-tests', + 'extensions/vscode-notebook-tests', + 'extensions/vscode-test-resolver', + 'remote', + 'remote/web', + 'test/automation', + 'test/integration/browser', + 'test/monaco', + 'test/smoke', +]; diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index be23c182e7..f35a2d236c 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -28,7 +28,41 @@ if (!/yarn[\w-.]*\.js$|yarnpkg$/.test(process.env['npm_execpath'])) { err = true; } +if (process.platform === 'win32') { + if (!hasSupportedVisualStudioVersion()) { + console.error('\033[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute.\033[0;0m'); + err = true; + } +} + if (err) { console.error(''); process.exit(1); } + +function hasSupportedVisualStudioVersion() { + const fs = require('fs'); + const path = require('path'); + // Translated over from + // https://source.chromium.org/chromium/chromium/src/+/master:build/vs_toolchain.py;l=140-175 + const supportedVersions = ['2019', '2017']; + + const availableVersions = []; + for (const version of supportedVersions) { + let vsPath = process.env[`vs${version}_install`]; + if (vsPath && fs.existsSync(vsPath)) { + availableVersions.push(version); + break; + } + const programFiles86Path = process.env['ProgramFiles(x86)']; + if (programFiles86Path) { + vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`; + const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools']; + if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { + availableVersions.push(version); + break; + } + } + } + return availableVersions.length; +} diff --git a/build/npm/update-all-grammars.js b/build/npm/update-all-grammars.js index c52eaa4fca..28b476ca5c 100644 --- a/build/npm/update-all-grammars.js +++ b/build/npm/update-all-grammars.js @@ -7,40 +7,41 @@ const cp = require('child_process'); const fs = require('fs'); const path = require('path'); -/** - * @param {string} location - */ -function updateGrammar(location) { - const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; - const result = cp.spawnSync(npm, ['run', 'update-grammar'], { - cwd: location, - stdio: 'inherit' +async function spawn(cmd, args, opts) { + return new Promise((c, e) => { + const child = cp.spawn(cmd, args, { shell: true, stdio: 'inherit', env: process.env, ...opts }); + child.on('close', code => code === 0 ? c() : e(`Returned ${code}`)); }); +} - if (result.error || result.status !== 0) { +async function main() { + await spawn('yarn', [], { cwd: 'extensions' }); + + for (const extension of fs.readdirSync('extensions')) { + try { + let packageJSON = JSON.parse(fs.readFileSync(path.join('extensions', extension, 'package.json')).toString()); + if (!(packageJSON && packageJSON.scripts && packageJSON.scripts['update-grammar'])) { + continue; + } + } catch { + continue; + } + + await spawn(`npm`, ['run', 'update-grammar'], { cwd: `extensions/${extension}` }); + } + + // run integration tests + + if (process.platform === 'win32') { + cp.spawn('.\\scripts\\test-integration.bat', [], { env: process.env, stdio: 'inherit' }); + } else { + cp.spawn('/bin/bash', ['./scripts/test-integration.sh'], { env: process.env, stdio: 'inherit' }); + } +} + +if (require.main === module) { + main().catch(err => { + console.error(err); process.exit(1); - } + }); } - -const allExtensionFolders = fs.readdirSync('extensions'); -const extensions = allExtensionFolders.filter(e => { - try { - let packageJSON = JSON.parse(fs.readFileSync(path.join('extensions', e, 'package.json')).toString()); - return packageJSON && packageJSON.scripts && packageJSON.scripts['update-grammar']; - } catch (e) { - return false; - } -}); - -console.log(`Updating ${extensions.length} grammars...`); - -extensions.forEach(extension => updateGrammar(`extensions/${extension}`)); - -// run integration tests - -if (process.platform === 'win32') { - cp.spawn('.\\scripts\\test-integration.bat', [], { env: process.env, stdio: 'inherit' }); -} else { - cp.spawn('/bin/bash', ['./scripts/test-integration.sh'], { env: process.env, stdio: 'inherit' }); -} - diff --git a/build/npm/update-theme.js b/build/npm/update-theme.js deleted file mode 100644 index cf267af407..0000000000 --- a/build/npm/update-theme.js +++ /dev/null @@ -1,82 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -var path = require('path'); -var fs = require('fs'); -var plist = require('fast-plist'); - -var mappings = { - "background": ["editor.background"], - "foreground": ["editor.foreground"], - "hoverHighlight": ["editor.hoverHighlightBackground"], - "linkForeground": ["editorLink.foreground"], - "selection": ["editor.selectionBackground"], - "inactiveSelection": ["editor.inactiveSelectionBackground"], - "selectionHighlightColor": ["editor.selectionHighlightBackground"], - "wordHighlight": ["editor.wordHighlightBackground"], - "wordHighlightStrong": ["editor.wordHighlightStrongBackground"], - "findMatchHighlight": ["editor.findMatchHighlightBackground", "peekViewResult.matchHighlightBackground"], - "currentFindMatchHighlight": ["editor.findMatchBackground"], - "findRangeHighlight": ["editor.findRangeHighlightBackground"], - "referenceHighlight": ["peekViewEditor.matchHighlightBackground"], - "lineHighlight": ["editor.lineHighlightBackground"], - "rangeHighlight": ["editor.rangeHighlightBackground"], - "caret": ["editorCursor.foreground"], - "invisibles": ["editorWhitespace.foreground"], - "guide": ["editorIndentGuide.background"], - "ansiBlack": ["terminal.ansiBlack"], "ansiRed": ["terminal.ansiRed"], "ansiGreen": ["terminal.ansiGreen"], "ansiYellow": ["terminal.ansiYellow"], - "ansiBlue": ["terminal.ansiBlue"], "ansiMagenta": ["terminal.ansiMagenta"], "ansiCyan": ["terminal.ansiCyan"], "ansiWhite": ["terminal.ansiWhite"], - "ansiBrightBlack": ["terminal.ansiBrightBlack"], "ansiBrightRed": ["terminal.ansiBrightRed"], "ansiBrightGreen": ["terminal.ansiBrightGreen"], - "ansiBrightYellow": ["terminal.ansiBrightYellow"], "ansiBrightBlue": ["terminal.ansiBrightBlue"], "ansiBrightMagenta": ["terminal.ansiBrightMagenta"], - "ansiBrightCyan": ["terminal.ansiBrightCyan"], "ansiBrightWhite": ["terminal.ansiBrightWhite"] -}; - -exports.update = function (srcName, destName) { - try { - console.log('reading ', srcName); - let result = {}; - let plistContent = fs.readFileSync(srcName).toString(); - let theme = plist.parse(plistContent); - let settings = theme.settings; - if (Array.isArray(settings)) { - let colorMap = {}; - for (let entry of settings) { - let scope = entry.scope; - if (scope) { - let parts = scope.split(',').map(p => p.trim()); - if (parts.length > 1) { - entry.scope = parts; - } - } else { - var entrySettings = entry.settings; - for (let entry in entrySettings) { - let mapping = mappings[entry]; - if (mapping) { - for (let newKey of mapping) { - colorMap[newKey] = entrySettings[entry]; - } - if (entry !== 'foreground' && entry !== 'background') { - delete entrySettings[entry]; - } - } - } - - } - } - result.name = theme.name; - result.tokenColors = settings; - result.colors = colorMap; - } - fs.writeFileSync(destName, JSON.stringify(result, null, '\t')); - } catch (e) { - console.log(e); - } -}; - -if (path.basename(process.argv[1]) === 'update-theme.js') { - exports.update(process.argv[2], process.argv[3]); -} diff --git a/build/package.json b/build/package.json index 106b498c2d..0dd9502c23 100644 --- a/build/package.json +++ b/build/package.json @@ -3,8 +3,11 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@azure/cosmos": "^3.9.3", + "@azure/storage-blob": "^12.4.0", "@types/ansi-colors": "^3.2.0", "@types/azure": "0.9.19", + "@types/byline": "^4.2.32", "@types/debounce": "^1.0.0", "@types/documentdb": "^1.10.5", "@types/eslint": "4.16.1", @@ -17,16 +20,17 @@ "@types/gulp-json-editor": "^2.2.31", "@types/gulp-rename": "^0.0.33", "@types/gulp-sourcemaps": "^0.0.32", - "@types/gulp-uglify": "^3.0.5", "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", - "@types/minimist": "^1.2.0", - "@types/mocha": "2.2.39", - "@types/node": "^12.11.7", + "@types/minimist": "^1.2.1", + "@types/mkdirp": "^1.0.1", + "@types/mocha": "^8.2.0", + "@types/node": "^12.19.9", + "@types/p-limit": "^2.2.0", + "@types/plist": "^3.0.2", "@types/pump": "^1.0.1", "@types/request": "^2.47.0", - "@types/rimraf": "^2.0.2", - "@types/terser": "^3.12.0", + "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", "@types/through2": "^2.0.34", "@types/underscore": "^1.8.9", @@ -35,36 +39,27 @@ "@typescript-eslint/parser": "^3.3.0", "applicationinsights": "1.0.8", "azure-storage": "^2.1.0", - "documentdb": "1.13.0", + "byline": "^5.0.0", + "colors": "^1.4.0", "electron-osx-sign": "^0.4.16", - "github-releases": "^0.4.1", - "gulp-azure-storage": "^0.11.1", - "gulp-bom": "^1.0.0", - "gulp-gzip": "^1.4.2", - "gulp-sourcemaps": "^1.11.0", - "gulp-uglify": "^3.0.0", + "esbuild": "^0.8.30", + "fs-extra": "^9.1.0", "iconv-lite-umd": "0.6.8", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", - "minimatch": "^3.0.4", - "minimist": "^1.2.3", - "request": "^2.85.0", - "rollup": "^1.20.3", + "mkdirp": "^1.0.4", + "p-limit": "^3.1.0", + "plist": "^3.0.1", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", - "terser": "4.3.8", - "typescript": "^4.2.0-dev.20201119", + "typescript": "4.2.0-dev.20201207", "vsce": "1.48.0", - "vscode-telemetry-extractor": "^1.6.0", - "xml2js": "^0.4.17" + "vscode-universal": "deepak1556/universal#61454d96223b774c53cda10f72c2098c0ce02d58" }, "scripts": { "compile": "tsc -p tsconfig.build.json", "watch": "tsc -p tsconfig.build.json --watch", - "postinstall": "npm run compile", "npmCheckJs": "tsc --noEmit" }, - "dependencies": { - "@azure/cosmos": "^3.9.3" - } + "dependencies": {} } diff --git a/build/yarn.lock b/build/yarn.lock index 7a3601fc6c..5ebc071fb0 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2,6 +2,73 @@ # yarn lockfile v1 +"@azure/abort-controller@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.2.tgz#822405c966b2aec16fb62c1b19d37eaccf231995" + integrity sha512-XUyTo+bcyxHEf+jlN2MXA7YU9nxVehaubngHV1MIZZaqYmZqykkoeAz/JMMEeR7t3TcyDwbFa3Zw8BZywmIx4g== + dependencies: + tslib "^2.0.0" + +"@azure/core-asynciterator-polyfill@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7" + integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg== + +"@azure/core-auth@^1.1.3": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.2.0.tgz#a5a181164e99f8446a3ccf9039345ddc9bb63bb9" + integrity sha512-KUl+Nwn/Sm6Lw5d3U90m1jZfNSL087SPcqHLxwn2T6PupNKmcgsEbDjHB25gDvHO4h7pBsTlrdJAY7dz+Qk8GA== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.0.0" + +"@azure/core-http@^1.2.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@azure/core-http/-/core-http-1.2.3.tgz#b1e459f6705df1f8d09bf6582292891c04bcace1" + integrity sha512-g5C1zUJO5dehP2Riv+vy9iCYoS1UwKnZsBVCzanScz9A83LbnXKpZDa9wie26G9dfXUhQoFZoFT8LYWhPKmwcg== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.1.3" + "@azure/core-tracing" "1.0.0-preview.9" + "@azure/logger" "^1.0.0" + "@opentelemetry/api" "^0.10.2" + "@types/node-fetch" "^2.5.0" + "@types/tunnel" "^0.0.1" + form-data "^3.0.0" + node-fetch "^2.6.0" + process "^0.11.10" + tough-cookie "^4.0.0" + tslib "^2.0.0" + tunnel "^0.0.6" + uuid "^8.3.0" + xml2js "^0.4.19" + +"@azure/core-lro@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-1.0.3.tgz#1ddfb4ecdb81ce87b5f5d972ffe2acbbc46e524e" + integrity sha512-Py2crJ84qx1rXkzIwfKw5Ni4WJuzVU7KAF6i1yP3ce8fbynUeu8eEWS4JGtSQgU7xv02G55iPDROifmSDbxeHA== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-http" "^1.2.0" + events "^3.0.0" + tslib "^2.0.0" + +"@azure/core-paging@^1.1.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@azure/core-paging/-/core-paging-1.1.3.tgz#3587c9898a0530cacb64bab216d7318468aa5efc" + integrity sha512-his7Ah40ThEYORSpIAwuh6B8wkGwO/zG7gqVtmSE4WAJ46e36zUDXTKReUCLBDc6HmjjApQQxxcRFy5FruG79A== + dependencies: + "@azure/core-asynciterator-polyfill" "^1.0.0" + +"@azure/core-tracing@1.0.0-preview.9": + version "1.0.0-preview.9" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz#84f3b85572013f9d9b85e1e5d89787aa180787eb" + integrity sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug== + dependencies: + "@opencensus/web-types" "0.0.7" + "@opentelemetry/api" "^0.10.2" + tslib "^2.0.0" + "@azure/cosmos@^3.9.3": version "3.9.3" resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-3.9.3.tgz#7e95ff92e5c3e9da7e8316bc50c9cc928be6c1d6" @@ -19,42 +86,51 @@ universal-user-agent "^6.0.0" uuid "^8.3.0" -"@dsherret/to-absolute-glob@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c" - integrity sha1-H2R13IvZdM6gei2vOGSzF7HdMyw= +"@azure/logger@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.0.1.tgz#19b333203d1b2931353d8879e814b64a7274837a" + integrity sha512-QYQeaJ+A5x6aMNu8BG5qdsVBnYBop9UMwgUvGihSjf1PdZZXB+c/oMdM2ajKwzobLBh9e9QuMQkN9iL+IxLBLA== dependencies: - is-absolute "^1.0.0" - is-negated-glob "^1.0.0" + tslib "^2.0.0" -"@gulp-sourcemaps/map-sources@1.X": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz#890ae7c5d8c877f6d384860215ace9d7ec945bda" - integrity sha1-iQrnxdjId/bThIYCFazp1+yUW9o= +"@azure/storage-blob@^12.4.0": + version "12.4.1" + resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.4.1.tgz#f2cc4a36a0df770b7b918ba89e72c02d72afbf2f" + integrity sha512-RH6ru8LbnCC+m1rlVLon6mYUXdHsTcyUXFCJAWRQQM7p0XOwVKPS+UiVk2tZXfvMWd3q/qT/meOrEbHEcp/c4g== dependencies: - normalize-path "^2.0.1" - through2 "^2.0.3" + "@azure/abort-controller" "^1.0.0" + "@azure/core-http" "^1.2.0" + "@azure/core-lro" "^1.0.2" + "@azure/core-paging" "^1.1.1" + "@azure/core-tracing" "1.0.0-preview.9" + "@azure/logger" "^1.0.0" + "@opentelemetry/api" "^0.10.2" + events "^3.0.0" + tslib "^2.0.0" -"@nodelib/fs.scandir@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz#7fa8fed654939e1a39753d286b48b4836d00e0eb" - integrity sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg== +"@malept/cross-spawn-promise@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" + integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== dependencies: - "@nodelib/fs.stat" "2.0.1" - run-parallel "^1.1.9" + cross-spawn "^7.0.1" -"@nodelib/fs.stat@2.0.1", "@nodelib/fs.stat@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz#814f71b1167390cfcb6a6b3d9cdeb0951a192c14" - integrity sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw== +"@opencensus/web-types@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@opencensus/web-types/-/web-types-0.0.7.tgz#4426de1fe5aa8f624db395d2152b902874f0570a" + integrity sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g== -"@nodelib/fs.walk@^1.2.1": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz#6a6450c5e17012abd81450eb74949a4d970d2807" - integrity sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ== +"@opentelemetry/api@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.10.2.tgz#9647b881f3e1654089ff7ea59d587b2d35060654" + integrity sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA== dependencies: - "@nodelib/fs.scandir" "2.1.1" - fastq "^1.6.0" + "@opentelemetry/context-base" "^0.10.2" + +"@opentelemetry/context-base@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.10.2.tgz#55bea904b2b91aa8a8675df9eaba5961bddb1def" + integrity sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw== "@types/ansi-colors@^3.2.0": version "3.2.0" @@ -68,6 +144,13 @@ dependencies: "@types/node" "*" +"@types/byline@^4.2.32": + version "4.2.32" + resolved "https://registry.yarnpkg.com/@types/byline/-/byline-4.2.32.tgz#9d35ec15968056118548412ee24c2c3026c997dc" + integrity sha512-qtlm/J6XOO9p+Ep/ZB5+mCFEDhzWDDHWU4a1eReN7lkPZXW9rkloq2jcAhvKKmlO5tL2GSvKROb+PTsNVhBiyQ== + dependencies: + "@types/node" "*" + "@types/caseless@*": version "0.12.1" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.1.tgz#9794c69c8385d0192acc471a540d1f8e0d16218a" @@ -86,7 +169,7 @@ resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.0.0.tgz#417560200331e1bb84d72da85391102c2fcd61b7" integrity sha1-QXVgIAMx4buE1y2oU5EQLC/NYbc= -"@types/debug@^4.1.4": +"@types/debug@^4.1.4", "@types/debug@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== @@ -133,6 +216,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^9.0.6": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.7.tgz#a9ef2ffdab043def080c5bec94c03402f793577f" + integrity sha512-YGq2A6Yc3bldrLUlm17VNWOnUbnEzJ9CMgOeLFtQF3HOCN5lQBO8VyjG00a5acA5NNSM30kHVGp1trZgnVgi1Q== + dependencies: + "@types/node" "*" + "@types/glob-stream@*": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc" @@ -195,14 +285,6 @@ dependencies: "@types/node" "*" -"@types/gulp-uglify@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/gulp-uglify/-/gulp-uglify-3.0.5.tgz#ddcbbb6bd15a84b8a6c5e2218c2efba98102d135" - integrity sha512-LD2b6gCPugrKI1W188nIp0gm+cAnGGwaTFpPdeZYVXwPHdoCQloy3du0JR62MeMjAwUwlcOb+SKYT6Qgw7yBiA== - dependencies: - "@types/node" "*" - "@types/uglify-js" "^2" - "@types/gulp@^4.0.5": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.5.tgz#f5f498d5bf9538364792de22490a12c0e6bc5eb4" @@ -217,11 +299,16 @@ resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.8.0.tgz#0369d3d0e1f35a6aec07cb4da2ee2bcda111367c" integrity sha512-/siF86XrwDKLuHe8l7h6NhrAWgLdgqbxmjZv9NvGWmgYRZoTipkjKiWb0SQHy/jcR+ee0GvbG6uGd+LEBMGNvA== -"@types/json-schema@*", "@types/json-schema@^7.0.3": +"@types/json-schema@*": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/json-schema@^7.0.3": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + "@types/mime@0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" @@ -232,25 +319,60 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/minimist@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" - integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= +"@types/minimist@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" + integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== -"@types/mocha@2.2.39": - version "2.2.39" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.39.tgz#f68d63db8b69c38e9558b4073525cf96c4f7a829" - integrity sha1-9o1j24tpw46VWLQHNSXPlsT3qCk= +"@types/mkdirp@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.1.tgz#0930b948914a78587de35458b86c907b6e98bbf6" + integrity sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q== + dependencies: + "@types/node" "*" + +"@types/mocha@^8.2.0": + version "8.2.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.1.tgz#f3f3ae4590c5386fc7c543aae9b78d4cf30ffee9" + integrity sha512-NysN+bNqj6E0Hv4CTGWSlPzMW6vTKjDpOteycDkV4IWBsO+PU48JonrPzV9ODjiI2XrjmA05KInLgF5ivZ/YGQ== + +"@types/node-fetch@^2.5.0": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" + integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" "@types/node@*": version "8.0.51" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@^12.11.7": - version "12.19.16" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.16.tgz#15753af35cbef636182d8d8ca55b37c8583cecb3" - integrity sha512-7xHmXm/QJ7cbK2laF+YYD7gb5MggHIIQwqyjin3bpEGiSuvScMQ5JZZXPvRipi1MwckTQbJZROMns/JxdnIL1Q== +"@types/node@^12.19.9": + version "12.20.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.1.tgz#63d36c10e162666f0107f247cdca76542c3c7472" + integrity sha512-tCkE96/ZTO+cWbln2xfyvd6ngHLanvVlJ3e5BeirJ3BYI5GbAyubIrmV4JjjugDly5D9fHjOL5MNsqsCnqwW6g== + +"@types/node@^14.14.21": + version "14.14.31" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" + integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== + +"@types/p-limit@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/p-limit/-/p-limit-2.2.0.tgz#94a608e9b258a6c6156a13d1a14fd720dba70b97" + integrity sha512-fGFbybl1r0oE9mqgfc2EHHUin9ZL5rbQIexWI6jYRU1ADVn4I3LHzT+g/kpPpZsfp8PB94CQ655pfAjNF8LP6A== + dependencies: + p-limit "*" + +"@types/plist@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" + integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== + dependencies: + "@types/node" "*" + xmlbuilder ">=11.0.1" "@types/pump@^1.0.1": version "1.0.1" @@ -276,21 +398,14 @@ dependencies: "@types/node" "*" -"@types/rimraf@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" - integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ== +"@types/rimraf@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.4.tgz#403887b0b53c6100a6c35d2ab24f6ccc042fec46" + integrity sha512-8gBudvllD2A/c0CcEX/BivIDorHFt5UI5m46TsNj8DjWCCTTZT74kEe4g+QsY7P/B9WdO98d82zZgXO/RQzu2Q== dependencies: "@types/glob" "*" "@types/node" "*" -"@types/terser@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@types/terser/-/terser-3.12.0.tgz#25e020fe9a7a6ae92ce46261f00ced67de6c12ac" - integrity sha512-J0Wy8A7ULEqVJftkWhrXZbH0iBk4tYuTj0gBiiveKaY9deNi6cCsxl0ApJ27ojqwYv51bvEw85lOb8Wt4ng9zA== - dependencies: - terser "*" - "@types/through2@^2.0.34": version "2.0.34" resolved "https://registry.yarnpkg.com/@types/through2/-/through2-2.0.34.tgz#9c2a259a238dace2a05a2f8e94b786961bc27ac4" @@ -310,12 +425,12 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.2.tgz#e0d481d8bb282ad8a8c9e100ceb72c995fb5e709" integrity sha512-vOVmaruQG5EatOU/jM6yU2uCp3Lz6mK1P5Ztu4iJjfM4SVHU9XYktPUQtKlIXuahqXHdEyUarMrBEwg5Cwu+bA== -"@types/uglify-js@^2": - version "2.6.31" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-2.6.31.tgz#c694755eeb6a1bb9f8f321f3ec37cc22ca4c4f6b" - integrity sha512-LjcyGt6CHsgZ0AoofnMwhyxo9hUqz2mgl6IcF+S8B1zdSTxHAvTO/1RPvBAHG3C1ZeAc+AoWA5mb3lDJKtM9Zg== +"@types/tunnel@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c" + integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A== dependencies: - source-map "^0.6.1" + "@types/node" "*" "@types/underscore@^1.8.9": version "1.8.9" @@ -427,39 +542,6 @@ dependencies: eslint-visitor-keys "^1.1.0" -acorn@4.X: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= - -acorn@^7.1.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -agent-base@5: - version "5.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" - integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== - -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^5.1.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -470,59 +552,6 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" - integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== - dependencies: - ansi-wrap "^0.1.0" - -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= - dependencies: - ansi-wrap "0.1.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-wrap@0.1.0, ansi-wrap@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= - -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -append-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" - integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= - dependencies: - buffer-equal "^1.0.0" - applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -539,124 +568,51 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-back@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - -array-differ@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" - integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= - -array-differ@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" - integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== - -array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -arrify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +asar@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" + integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== + dependencies: + chromium-pickle-js "^0.2.0" + commander "^5.0.0" + glob "^7.1.6" + minimatch "^3.0.4" + optionalDependencies: + "@types/glob" "^7.1.1" asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - integrity sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y= + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4= - -aws4@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" - integrity sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w== - aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== azure-storage@^2.1.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.6.0.tgz#84747ee54a4bd194bb960f89f3eff89d67acf1cf" - integrity sha1-hHR+5UpL0ZS7lg+J8+/4nWes8c8= - dependencies: - browserify-mime "~1.2.9" - extend "~1.2.1" - json-edm-parser "0.1.2" - md5.js "1.3.4" - readable-stream "~2.0.0" - request "~2.81.0" - underscore "~1.8.3" - uuid "^3.0.0" - validator "~3.35.0" - xml2js "0.2.7" - xmlbuilder "0.4.3" - -azure-storage@^2.10.2: version "2.10.3" resolved "https://registry.yarnpkg.com/azure-storage/-/azure-storage-2.10.3.tgz#c5966bf929d87587d78f6847040ea9a4b1d4a50a" integrity sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ== @@ -679,58 +635,27 @@ balanced-match@^1.0.0: integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= base64-js@^1.2.3: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - integrity sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40= + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" -beeper@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" - integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak= - -binary-search-bounds@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz#5ff8616d6dd2ca5388bc85b2d6266e2b9da502dc" - integrity sha1-X/hhbW3SylOIvIWy1iZuK52lAtw= - bluebird@^3.5.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -boolbase@~1.0.0: +boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= - dependencies: - hoek "2.x.x" - -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - integrity sha1-T4owBctKfjiJ90kDD9JbluAdLjE= - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - integrity sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw== - dependencies: - hoek "4.x.x" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -739,13 +664,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - browserify-mime@~1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" @@ -769,7 +687,7 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-equal@^1.0.0: +buffer-equal@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= @@ -779,169 +697,83 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - builtin-modules@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" - integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" + integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== -bytes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= +cheerio-select-tmp@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" + integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" + css-select "^3.1.2" + css-what "^4.0.0" + domelementtype "^2.1.0" + domhandler "^4.0.0" + domutils "^2.4.4" cheerio@^1.0.0-rc.1: - version "1.0.0-rc.2" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" - integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs= + version "1.0.0-rc.5" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" + integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.0" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash "^4.15.0" - parse5 "^3.0.1" + cheerio-select-tmp "^0.1.0" + dom-serializer "~1.2.0" + domhandler "^4.0.0" + entities "~2.1.0" + htmlparser2 "^6.0.0" + parse5 "^6.0.0" + parse5-htmlparser2-tree-adapter "^6.0.0" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" +chromium-pickle-js@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" + integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= -clone-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -clone-stats@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" - integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= +colors@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= - -clone@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -cloneable-readable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" - integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== - dependencies: - inherits "^2.0.1" - process-nextick-args "^2.0.0" - readable-stream "^2.3.5" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-block-writer@9.4.1: - version "9.4.1" - resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-9.4.1.tgz#1448fca79dfc7a3649000f4c85be6bc770604c4c" - integrity sha512-LHAB+DL4YZDcwK8y/kAxZ0Lf/ncwLh/Ux4cTVWbPwIdrf1gPxXiPcwpz8r8/KqXu1aD+Raz46EOxDjFlbyO6bA== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -colors@^1.1.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794" - integrity sha512-s8+wktIuDSLffCywiwSxQOMqtPxML11a/dtHE17tMn4B1MSWw/C22EKf7M2KGUBcDaVFEGT+S8N02geDXeuNKg== - -combined-stream@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" - integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= - dependencies: - delayed-stream "~1.0.0" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - integrity sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk= - dependencies: - delayed-stream "~1.0.0" - -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -command-line-args@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.1.1.tgz#88e793e5bb3ceb30754a86863f0401ac92fd369a" - integrity sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg== +commander@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= dependencies: - array-back "^3.0.1" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - -commander@^2.20.0, commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + graceful-readlink ">= 1.0.0" commander@^2.8.1: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== compare-version@^0.1.2: version "0.1.2" @@ -953,63 +785,35 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -convert-source-map@1.X: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== - dependencies: - safe-buffer "~5.1.1" - -convert-source-map@^1.5.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= +cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - boom "2.x.x" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" -cryptiles@3.x.x: +css-select@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" - integrity sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4= + resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" + integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== dependencies: - boom "5.x.x" + boolbase "^1.0.0" + css-what "^4.0.0" + domhandler "^4.0.0" + domutils "^2.4.3" + nth-check "^2.0.0" -css-select@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - -css-what@2.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" - integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== - -css@2.X: - version "2.2.4" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" - integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== - dependencies: - inherits "^2.0.3" - source-map "^0.6.1" - source-map-resolve "^0.5.2" - urix "^0.1.0" +css-what@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" + integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== dashdash@^1.12.0: version "1.14.1" @@ -1018,63 +822,20 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -dateformat@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" - integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= - -debug-fabulous@0.0.X: - version "0.0.4" - resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-0.0.4.tgz#fa071c5d87484685424807421ca4b16b0b1a0763" - integrity sha1-+gccXYdIRoVCSAdCHKSxawsaB2M= - dependencies: - debug "2.X" - lazy-debug-legacy "0.0.X" - object-assign "4.1.0" - -debug@2.X, debug@^2.6.8: +debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@^4.1.1: +debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -delayed-stream@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.6.tgz#a2646cb7ec3d5d7774614670a7a65de0c173edbc" - integrity sha1-omRst+w9XXd0YUZwp6Zd4MFz7bw= - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1085,11 +846,6 @@ denodeify@^1.2.1: resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= -detect-newline@2.X: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" @@ -1102,102 +858,58 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== +dir-compare@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" + integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== dependencies: - path-type "^4.0.0" + buffer-equal "1.0.0" + colors "1.0.3" + commander "2.9.0" + minimatch "3.0.4" -documentdb@1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/documentdb/-/documentdb-1.13.0.tgz#bba6f03150b2f42498cec4261bc439d834a33f8b" - integrity sha1-u6bwMVCy9CSYzsQmG8Q52DSjP4s= +dom-serializer@^1.0.1, dom-serializer@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== dependencies: - binary-search-bounds "2.0.3" - priorityqueuejs "1.0.0" - semaphore "1.0.5" - underscore "1.8.3" + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" -dom-serializer@0, dom-serializer@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" - integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= +domelementtype@^2.0.1, domelementtype@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + +domhandler@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" + integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== dependencies: - domelementtype "~1.1.1" - entities "~1.1.1" + domelementtype "^2.1.0" -domelementtype@1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.2.1.tgz#578558ef23befac043a1abb0db07635509393479" - integrity sha512-SQVCLFS2E7G5CRCMdn6K9bIhRj1bS6QBWZfF0TUPh4V/BbqrQ619IdSS3/izn0FZ+9l+uODzaZjb08fjOfablA== - -domelementtype@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" - integrity sha1-sXrtguirWeUt2cGbF1bg/BhyBMI= - -domelementtype@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" - integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== +domutils@^2.4.3, domutils@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" + integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== dependencies: - domelementtype "1" - -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -duplexer2@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" - integrity sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds= - dependencies: - readable-stream "~1.1.9" - -duplexer@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" + dom-serializer "^1.0.1" + domelementtype "^2.0.1" + domhandler "^4.0.0" ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - integrity sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU= + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" + safer-buffer "^2.1.0" electron-osx-sign@^0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.16.tgz#0be8e579b2d9fa4c12d2a21f063898294b3434aa" - integrity sha512-ziMWfc3NmQlwnWLW6EaZq8nH2BWVng/atX5GWsGwhexJYpdW6hsg//MkAfRTRx1kR3Veiqkeiog1ibkbA4x0rg== + version "0.4.17" + resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.17.tgz#2727ca0c79e1e4e5ccd3861fb3da9c3c913b006c" + integrity sha512-wUJPmZJQCs1zgdlQgeIpRcvrf7M5/COQaOV68Va1J/SgmWx5KL2otgg+fAae7luw6qz9R8Gvu/Qpe9tAOu/3xQ== dependencies: bluebird "^3.5.0" compare-version "^0.1.2" @@ -1206,61 +918,32 @@ electron-osx-sign@^0.4.16: minimist "^1.2.0" plist "^3.0.1" -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -entities@^1.1.1, entities@~1.1.1: +entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +esbuild@^0.8.30: + version "0.8.47" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.47.tgz#5d5c59b7dcb8a20dfadf65a985e5e5ca7b24eff2" + integrity sha512-4C9pInguP36c9CRDMSb6W1KrMfXrLIQVtI02Vglc43RBV0Dw49ODEnP6985mrz5iqCdQ2Cxmb9i670J/s3DBPA== eslint-scope@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" - integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" eslint-utils@^2.0.0: @@ -1271,121 +954,62 @@ eslint-utils@^2.0.0: eslint-visitor-keys "^1.1.0" eslint-visitor-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== -event-stream@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" +events@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== -extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extend@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-1.2.1.tgz#a0f5fd6cfc83a5fe49ef698d60ec8a624dd4576c" - integrity sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w= - -extend@~3.0.0, extend@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= - -extsprintf@1.3.0, extsprintf@^1.2.0: +extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= -fancy-log@^1.1.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1" - integrity sha1-9BEl49hPLn2JpD0G2VjI94vha+E= - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - time-stamp "^1.0.0" - -fancy-log@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" - integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - parse-node-version "^1.0.0" - time-stamp "^1.0.0" - -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602" - integrity sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg== - dependencies: - "@nodelib/fs.stat" "^2.0.1" - "@nodelib/fs.walk" "^1.2.1" - glob-parent "^5.0.0" - is-glob "^4.0.1" - merge2 "^1.2.3" - micromatch "^4.0.2" - fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fastq@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2" - integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA== - dependencies: - reusify "^1.0.0" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -1393,57 +1017,18 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flush-write-stream@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE= +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== dependencies: asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" + combined-stream "^1.0.8" mime-types "^2.1.12" form-data@~2.3.2: @@ -1455,27 +1040,15 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== +fs-extra@^9.0.1, fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: + at-least-node "^1.0.0" graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-mkdirp-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" - integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= - dependencies: - graceful-fs "^4.1.11" - through2 "^2.0.3" + jsonfile "^6.0.1" + universalify "^2.0.0" fs.realpath@^1.0.0: version "1.0.0" @@ -1487,11 +1060,6 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1499,60 +1067,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -github-releases@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/github-releases/-/github-releases-0.4.1.tgz#4a13bdf85c4161344271db3d81db08e7379102ff" - integrity sha1-ShO9+FxBYTRCcds9gdsI5zeRAv8= - dependencies: - minimatch "3.0.4" - optimist "0.6.1" - prettyjson "1.2.1" - request "2.81.0" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" - integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== - dependencies: - is-glob "^4.0.1" - -glob-stream@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" - integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= - dependencies: - extend "^3.0.0" - glob "^7.1.1" - glob-parent "^3.1.0" - is-negated-glob "^1.0.0" - ordered-read-streams "^1.0.0" - pumpify "^1.3.5" - readable-stream "^2.1.5" - remove-trailing-separator "^1.0.1" - to-absolute-glob "^2.0.0" - unique-stream "^2.0.2" - -glob@^7.0.6: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.1, glob@^7.1.6: +glob@^7.0.6, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -1564,180 +1079,21 @@ glob@^7.1.1, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globby@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" - integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== - dependencies: - "@types/glob" "^7.1.1" - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" - slash "^3.0.0" - -glogg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.1.tgz#dcf758e44789cc3f3d32c1f3562a3676e6a34810" - integrity sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw== - dependencies: - sparkles "^1.0.0" - -graceful-fs@4.X: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= - -graceful-fs@^4.0.0, graceful-fs@^4.1.11: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" - integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -gulp-azure-storage@^0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/gulp-azure-storage/-/gulp-azure-storage-0.11.1.tgz#0e5f5d0f789da11206f1e5a9311a6cf7107877d7" - integrity sha512-csOwItwZV1P9GLsORVQy+CFwjYDdHNrBol89JlHdlhGx0fTgJBc1COTRZbjGRyRjgdUuVguo3YLl4ToJ10/SIQ== - dependencies: - azure-storage "^2.10.2" - delayed-stream "0.0.6" - event-stream "3.3.4" - mime "^1.3.4" - progress "^1.1.8" - queue "^3.0.10" - streamifier "^0.1.1" - vinyl "^2.2.0" - vinyl-fs "^3.0.3" - yargs "^15.3.0" - -gulp-bom@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulp-bom/-/gulp-bom-1.0.0.tgz#38a183a07187bd57a7922d37977441f379df2abf" - integrity sha1-OKGDoHGHvVenki03l3RB83nfKr8= - dependencies: - gulp-util "^3.0.0" - through2 "^2.0.0" - -gulp-gzip@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/gulp-gzip/-/gulp-gzip-1.4.2.tgz#0422a94014248655b5b1a9eea1c2abee1d4f4337" - integrity sha512-ZIxfkUwk2XmZPTT9pPHrHUQlZMyp9nPhg2sfoeN27mBGpi7OaHnOD+WCN41NXjfJQ69lV1nQ9LLm1hYxx4h3UQ== - dependencies: - ansi-colors "^1.0.1" - bytes "^3.0.0" - fancy-log "^1.3.2" - plugin-error "^1.0.0" - stream-to-array "^2.3.0" - through2 "^2.0.3" - -gulp-sourcemaps@^1.11.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.12.1.tgz#b437d1f3d980cf26e81184823718ce15ae6597b6" - integrity sha1-tDfR89mAzyboEYSCNxjOFa5ll7Y= - dependencies: - "@gulp-sourcemaps/map-sources" "1.X" - acorn "4.X" - convert-source-map "1.X" - css "2.X" - debug-fabulous "0.0.X" - detect-newline "2.X" - graceful-fs "4.X" - source-map "~0.6.0" - strip-bom "2.X" - through2 "2.X" - vinyl "1.X" - -gulp-uglify@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.2.tgz#5f5b2e8337f879ca9dec971feb1b82a5a87850b0" - integrity sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg== - dependencies: - array-each "^1.0.1" - extend-shallow "^3.0.2" - gulplog "^1.0.0" - has-gulplog "^0.1.0" - isobject "^3.0.1" - make-error-cause "^1.1.1" - safe-buffer "^5.1.2" - through2 "^2.0.0" - uglify-js "^3.0.5" - vinyl-sourcemaps-apply "^0.2.0" - -gulp-util@^3.0.0: - version "3.0.8" - resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" - integrity sha1-AFTh50RQLifATBh8PsxQXdVLu08= - dependencies: - array-differ "^1.0.0" - array-uniq "^1.0.2" - beeper "^1.0.0" - chalk "^1.0.0" - dateformat "^2.0.0" - fancy-log "^1.1.0" - gulplog "^1.0.0" - has-gulplog "^0.1.0" - lodash._reescape "^3.0.0" - lodash._reevaluate "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.template "^3.0.0" - minimist "^1.1.0" - multipipe "^0.1.2" - object-assign "^3.0.0" - replace-ext "0.0.1" - through2 "^2.0.0" - vinyl "^0.5.0" - -gulplog@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" - integrity sha1-4oxNRdBey77YGDY86PnFkmIp/+U= - dependencies: - glogg "^1.0.0" - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4= +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio= - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - har-validator@~5.1.3: version "5.1.5" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" @@ -1746,25 +1102,6 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-gulplog@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" - integrity sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4= - dependencies: - sparkles "^1.0.0" - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -1773,63 +1110,23 @@ has@^1.0.3: function-bind "^1.1.1" hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= +htmlparser2@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" + integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - integrity sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ== - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= - -hoek@4.x.x: - version "4.2.1" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" - integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== - -htmlparser2@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" - integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ== - dependencies: - domelementtype "^1.3.0" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.0.6" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.4.4" + entities "^2.0.0" http-signature@~1.2.0: version "1.2.0" @@ -1840,24 +1137,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" - integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== - dependencies: - agent-base "5" - debug "4" - iconv-lite-umd@0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.8.tgz#5ad310ec126b260621471a2d586f7f37b9958ec0" integrity sha512-zvXJ5gSwMC9JD3wDzH8CoZGc1pbiJn12Tqjk8BXYCnYz3hYL5GRjHW8LEykjXhV9WgNGI4rgpgHcbIiBfrRq6A== -ignore@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558" - integrity sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1866,70 +1150,23 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.0: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - is-core-module@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" - integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== dependencies: has "^1.0.3" -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -1942,28 +1179,6 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= -is-negated-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" - integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= - -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - is-reference@^1.1.2: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -1971,59 +1186,11 @@ is-reference@^1.1.2: dependencies: "@types/estree" "*" -is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-utf8@^0.2.0, is-utf8@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-valid-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" - integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= - -is-windows@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2036,10 +1203,10 @@ isbinaryfile@^3.0.2: dependencies: buffer-alloc "^1.2.0" -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isstream@~0.1.2: version "0.1.2" @@ -2063,11 +1230,6 @@ json-edm-parser@0.1.2: dependencies: jsonparse "~1.2.0" -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -2078,40 +1240,25 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" - json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= jsonc-parser@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" - integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" + integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - jsonparse@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd" @@ -2127,154 +1274,19 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -lazy-debug-legacy@0.0.X: - version "0.0.1" - resolved "https://registry.yarnpkg.com/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz#537716c0776e4cf79e3ed1b621f7658c2911b1b1" - integrity sha1-U3cWwHduTPeePtG2IfdljCkRsbE= - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= - dependencies: - readable-stream "^2.0.5" - -lead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" - integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= - dependencies: - flush-write-stream "^1.0.2" - linkify-it@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" - integrity sha1-2UpGSPmxwXnWT6lykSaL22zpQ08= + version "2.2.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" + integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== dependencies: uc.micro "^1.0.1" -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= - -lodash._basetostring@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" - integrity sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U= - -lodash._basevalues@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" - integrity sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc= - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw= - -lodash._reescape@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" - integrity sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo= - -lodash._reevaluate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" - integrity sha1-WLx0xAZklTrgsSTYBpltrKQx4u0= - -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - -lodash._root@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" - integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI= - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - -lodash.escape@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" - integrity sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg= - dependencies: - lodash._root "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo= - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - -lodash.template@^3.0.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" - integrity sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8= - dependencies: - lodash._basecopy "^3.0.0" - lodash._basetostring "^3.0.0" - lodash._basevalues "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash._reinterpolate "^3.0.0" - lodash.escape "^3.0.0" - lodash.keys "^3.0.0" - lodash.restparam "^3.0.0" - lodash.templatesettings "^3.0.0" - -lodash.templatesettings@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" - integrity sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU= - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.escape "^3.0.0" - lodash.unescape@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= -lodash@^4.15.0, lodash@^4.17.10: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -lodash@^4.17.15: +lodash@^4.17.10, lodash@^4.17.15: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -2293,23 +1305,6 @@ magic-string@^0.25.2: dependencies: sourcemap-codec "^1.4.4" -make-error-cause@^1.1.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" - integrity sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0= - dependencies: - make-error "^1.2.0" - -make-error@^1.2.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" - integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== - -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= - markdown-it@^8.3.1: version "8.4.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" @@ -2334,61 +1329,19 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -merge2@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" - integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== +mime-db@1.46.0: + version "1.46.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" + integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.29" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" + integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== dependencies: - braces "^3.0.1" - picomatch "^2.0.5" + mime-db "1.46.0" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - integrity sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE= - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - integrity sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo= - dependencies: - mime-db "~1.30.0" - -mime-types@~2.1.17: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@~2.1.19: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@^1.3.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== - -mime@^1.4.1: +mime@^1.3.4, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -2400,48 +1353,30 @@ minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.3.tgz#3db5c0765545ab8637be71f333a104a965a9ca3f" - integrity sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw== +minimist@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -multimatch@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" - integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== - dependencies: - "@types/minimatch" "^3.0.3" - array-differ "^3.0.0" - array-union "^2.1.0" - arrify "^2.0.1" - minimatch "^3.0.4" - -multipipe@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" - integrity sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s= - dependencies: - duplexer2 "0.0.2" - mute-stream@~0.0.4: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== node-abort-controller@^1.0.4: version "1.1.0" @@ -2453,89 +1388,25 @@ node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= +nth-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" + integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== dependencies: - remove-trailing-separator "^1.0.1" - -now-and-later@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" - integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== - dependencies: - once "^1.3.2" - -nth-check@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -oauth-sign@~0.8.1, oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= + boolbase "^1.0.0" oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" - integrity sha1-ejs9DpgGPUP0wD8uiubNUahog6A= - -object-assign@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" - integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= - -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.0.4, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -optimist@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -ordered-read-streams@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" - integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= - dependencies: - readable-stream "^2.0.1" - os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -2554,29 +1425,12 @@ osenv@^0.1.3: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +p-limit@*, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parse-node-version@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + yocto-queue "^0.1.0" parse-semver@^1.1.1: version "1.1.1" @@ -2585,65 +1439,43 @@ parse-semver@^1.1.1: dependencies: semver "^5.1.0" -parse5@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" - integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== +parse5-htmlparser2-tree-adapter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== dependencies: - "@types/node" "*" + parse5 "^6.0.1" -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +parse5@^6.0.0, parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= - dependencies: - through "~2.3" - pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.5: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== - plist@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c" @@ -2653,81 +1485,26 @@ plist@^3.0.1: xmlbuilder "^9.0.7" xmldom "0.1.x" -plugin-error@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" - integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== - dependencies: - ansi-colors "^1.0.1" - arr-diff "^4.0.0" - arr-union "^3.1.0" - extend-shallow "^3.0.2" - -prettyjson@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" - integrity sha1-/P+rQdGcq0365eV15kJGYZsS0ok= - dependencies: - colors "^1.1.2" - minimist "^1.2.0" - -priorityqueuejs@1.0.0, priorityqueuejs@^1.0.0: +priorityqueuejs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" integrity sha1-LuTyPCVgkT4IwHzlzN1t498sWvg= -process-nextick-args@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -psl@^1.1.28: +psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.5: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -2738,28 +1515,11 @@ q@^1.0.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= - -qs@~6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== - qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -queue@^3.0.10: - version "3.1.0" - resolved "https://registry.yarnpkg.com/queue/-/queue-3.1.0.tgz#6c49d01f009e2256788789f2bffac6b8b9990585" - integrity sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU= - dependencies: - inherits "~2.0.0" - read@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" @@ -2767,51 +1527,15 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^2.1.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a" - integrity sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg== +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -2824,94 +1548,6 @@ readable-stream@~2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" -remove-bom-buffer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" - integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== - dependencies: - is-buffer "^1.1.5" - is-utf8 "^0.2.1" - -remove-bom-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" - integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM= - dependencies: - remove-bom-buffer "^3.0.0" - safe-buffer "^5.1.0" - through2 "^2.0.3" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= - -replace-ext@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" - integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== - -request@2.81.0, request@~2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA= - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -request@^2.85.0: - version "2.85.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" - integrity sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - hawk "~6.0.2" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - stringstream "~0.0.5" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - request@^2.86.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -2938,28 +1574,6 @@ request@^2.86.0: tunnel-agent "^0.6.0" uuid "^3.3.2" -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-options@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" - integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE= - dependencies: - value-or-function "^3.0.0" - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - resolve@^1.11.0, resolve@^1.11.1: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" @@ -2968,11 +1582,6 @@ resolve@^1.11.0, resolve@^1.11.1: is-core-module "^2.1.0" path-parse "^1.0.6" -reusify@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - rollup-plugin-commonjs@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb" @@ -3002,44 +1611,15 @@ rollup-pluginutils@^2.8.1: dependencies: estree-walker "^0.6.1" -rollup@^1.20.3: - version "1.32.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4" - integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A== - dependencies: - "@types/estree" "*" - "@types/node" "*" - acorn "^7.1.0" - -run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - -safe-buffer@^5.0.1, safe-buffer@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== - -safe-buffer@^5.1.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@^5.1.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -sax@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.2.tgz#735ffaa39a1cff8ffb9598f0223abdb03a9fb2ea" - integrity sha1-c1/6o5oc/4/7lZjwIjq9sDqfsuo= +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sax@0.5.x: version "0.5.8" @@ -3051,20 +1631,15 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -semaphore@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.0.5.tgz#b492576e66af193db95d65e25ec53f5f19798d60" - integrity sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA= - semaphore@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== semver@^5.1.0, semver@^5.3.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== semver@^6.3.0: version "6.3.0" @@ -3078,245 +1653,55 @@ semver@^7.3.2: dependencies: lru-cache "^6.0.0" -set-blocking@^2.0.0: +shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" -slash@^3.0.0: +shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= - dependencies: - hoek "2.x.x" - -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - integrity sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg== - dependencies: - hoek "4.x.x" - -source-map-resolve@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" - integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== - dependencies: - atob "^2.1.1" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.12: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== -sparkles@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" - integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== - -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - integrity sha1-US322mKHFEMW3EwY/hzx2UBzm+M= + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" ecc-jsbn "~0.1.1" + getpass "^0.1.1" jsbn "~0.1.0" + safer-buffer "^2.0.2" tweetnacl "~0.14.0" -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - duplexer "~0.1.1" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -stream-to-array@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" - integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M= - dependencies: - any-promise "^1.1.0" - -streamifier@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" - integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.trimend@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz#6ddd9a8796bc714b489a3ae22246a208f37bfa46" - integrity sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -string.prototype.trimstart@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz#22d45da81015309cd0cdd79787e8919fc5c613e7" - integrity sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -string_decoder@^1.1.1, string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" + safe-buffer "~5.2.0" string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -stringstream@~0.0.4, stringstream@~0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" - integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-bom@2.X: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -terser@*: - version "4.2.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.1.tgz#1052cfe17576c66e7bc70fcc7119f22b155bdac1" - integrity sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@4.3.8: - version "4.3.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.8.tgz#707f05f3f4c1c70c840e626addfdb1c158a17136" - integrity sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -through2-filter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" - integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== - dependencies: - through2 "~2.0.0" - xtend "~4.0.0" - -through2@2.X, through2@^2.0.0, through2@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" - integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= - dependencies: - readable-stream "^2.1.5" - xtend "~4.0.1" - -through2@~2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through@2, through@~2.3, through@~2.3.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -time-stamp@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= - tmp@0.0.29: version "0.0.29" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" @@ -3324,41 +1709,14 @@ tmp@0.0.29: dependencies: os-tmpdir "~1.0.1" -to-absolute-glob@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" - integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== dependencies: - is-absolute "^1.0.0" - is-negated-glob "^1.0.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-through@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" - integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY= - dependencies: - through2 "^2.0.3" - -tough-cookie@~2.3.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - integrity sha1-C2GKVWW23qkL80JdBNVe3EdadWE= - dependencies: - punycode "^1.4.1" - -tough-cookie@~2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" - integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== - dependencies: - punycode "^1.4.1" + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" tough-cookie@~2.5.0: version "2.5.0" @@ -3368,24 +1726,10 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -ts-morph@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-3.1.3.tgz#bbfa1d14481ee23bdd1c030340ccf4a243cfc844" - integrity sha512-CwjgyJTtd3f8vBi7Vr0IOgdOY6Wi/Tq0MhieXOE2B5ns5WWRD7BwMNHtv+ZufKI/S2U/lMrh+Q3bOauE4tsv2g== - dependencies: - "@dsherret/to-absolute-glob" "^2.0.2" - code-block-writer "9.4.1" - fs-extra "^8.1.0" - glob-parent "^5.0.0" - globby "^10.0.1" - is-negated-glob "^1.0.0" - multimatch "^4.0.0" - typescript "^3.0.1" - tslib@^1.8.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.0: version "2.0.3" @@ -3393,9 +1737,9 @@ tslib@^2.0.0: integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + version "3.20.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" + integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== dependencies: tslib "^1.8.1" @@ -3411,6 +1755,11 @@ tunnel@0.0.4: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.4.tgz#2d3785a158c174c9a16dc2c046ec5fc5f1742213" integrity sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM= +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -3424,38 +1773,20 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@^3.0.1: - version "3.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" - integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@4.2.0-dev.20201207: + version "4.2.0-dev.20201207" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.0-dev.20201207.tgz#19a34bc7d2d42a7467c512c63f135587ac848807" + integrity sha512-fPHBDi/fgdX4WiRC7cFVv/aL069PgUaDWuLYUSHatWZujz/Lkc9bkf/zL3rKdNSCxlNKAMs3fhJv/yompOphZA== -typescript@^4.2.0-dev.20201119: - version "4.2.0-dev.20201119" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.0-dev.20201119.tgz#d4a43511cd9931adac05e1a47b6425f6b0e76cc3" - integrity sha512-HIgv+D/0VpVYRTbcVVf9oac/0GtLKMqaufTcPgohNaFWlCOh4lq8syefANgENXTG5Q4VEC6xwDGzHW6EJAVr3A== - -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== +typescript@^4.1.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" + integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" - integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== - -uglify-js@^3.0.5: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== - dependencies: - commander "~2.20.0" - source-map "~0.6.1" - -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== underscore@1.8.3, underscore@~1.8.3: version "1.8.3" @@ -3463,40 +1794,32 @@ underscore@1.8.3, underscore@~1.8.3: integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= underscore@^1.8.3: - version "1.9.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" - integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== - -unique-stream@^2.0.2: - version "2.3.1" - resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" - integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== - dependencies: - json-stable-stringify-without-jsonify "^1.0.1" - through2-filter "^3.0.0" + version "1.12.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.0.tgz#4814940551fc80587cef7840d1ebb0f16453be97" + integrity sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ== universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== -universalify@^0.1.0: +universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - url-join@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" @@ -3507,17 +1830,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== - -uuid@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" - integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== - -uuid@^3.3.2: +uuid@^3.0.0, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -3527,21 +1840,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== -validator@~3.35.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-3.35.0.tgz#3f07249402c1fc8fc093c32c6e43d72a79cca1dc" - integrity sha1-PwcklALB/I/Ak8MsbkPXKnnModw= - validator@~9.4.1: version "9.4.1" resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== -value-or-function@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" - integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -3551,79 +1854,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vinyl-fs@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" - integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== - dependencies: - fs-mkdirp-stream "^1.0.0" - glob-stream "^6.1.0" - graceful-fs "^4.0.0" - is-valid-glob "^1.0.0" - lazystream "^1.0.0" - lead "^1.0.0" - object.assign "^4.0.4" - pumpify "^1.3.5" - readable-stream "^2.3.3" - remove-bom-buffer "^3.0.0" - remove-bom-stream "^1.2.0" - resolve-options "^1.1.0" - through2 "^2.0.0" - to-through "^2.0.0" - value-or-function "^3.0.0" - vinyl "^2.0.0" - vinyl-sourcemap "^1.1.0" - -vinyl-sourcemap@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" - integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY= - dependencies: - append-buffer "^1.0.2" - convert-source-map "^1.5.0" - graceful-fs "^4.1.6" - normalize-path "^2.1.1" - now-and-later "^2.0.0" - remove-bom-buffer "^3.0.0" - vinyl "^2.0.0" - -vinyl-sourcemaps-apply@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" - integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU= - dependencies: - source-map "^0.5.1" - -vinyl@1.X: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" - integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" - integrity sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4= - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^2.0.0, vinyl@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" - integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - vsce@1.48.0: version "1.48.0" resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.48.0.tgz#31c1a4c6909c3b8bdc48b3d32cc8c8e94c7113a2" @@ -3647,22 +1877,19 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-ripgrep@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.6.2.tgz#fb912c7465699f10ce0218a6676cc632c77369b4" - integrity sha512-jkZEWnQFcE+QuQFfxQXWcWtDafTmgkp3DjMKawDkajZwgnDlGKpFp15ybKrZNVTi1SLEF/12BzxYSZVVZ2XrkA== +vscode-universal@deepak1556/universal#61454d96223b774c53cda10f72c2098c0ce02d58: + version "0.0.2" + resolved "https://codeload.github.com/deepak1556/universal/tar.gz/61454d96223b774c53cda10f72c2098c0ce02d58" dependencies: - https-proxy-agent "^4.0.0" - proxy-from-env "^1.1.0" - -vscode-telemetry-extractor@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vscode-telemetry-extractor/-/vscode-telemetry-extractor-1.6.0.tgz#e9d9c1d24863cce8d3d715f0287de3b31eb90c56" - integrity sha512-zSxvkbyAMa1lTRGIHfGg7gW2e9Sey+2zGYD19uNWCsVEfoXAr2NB6uzb0sNHtbZ2SSqxSePmFXzBAavsudT5fw== - dependencies: - command-line-args "^5.1.1" - ts-morph "^3.1.3" - vscode-ripgrep "^1.6.2" + "@malept/cross-spawn-promise" "^1.1.0" + "@types/debug" "^4.1.5" + "@types/fs-extra" "^9.0.6" + "@types/node" "^14.14.21" + asar "^3.0.3" + debug "^4.3.1" + dir-compare "^2.4.0" + fs-extra "^9.0.1" + typescript "^4.1.3" vso-node-api@6.1.2-preview: version "6.1.2-preview" @@ -3674,37 +1901,18 @@ vso-node-api@6.1.2-preview: typed-rest-client "^0.9.0" underscore "^1.8.3" -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" + isexe "^2.0.0" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xml2js@0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.7.tgz#1838518bb01741cae0878bab4915e494c32306af" - integrity sha1-GDhRi7AXQcrgh4urSRXklMMjBq8= - dependencies: - sax "0.5.2" - xml2js@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.2.8.tgz#9b81690931631ff09d1957549faf54f4f980b3c2" @@ -3712,79 +1920,39 @@ xml2js@0.2.8: dependencies: sax "0.5.x" -xml2js@^0.4.17: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== +xml2js@^0.4.19: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== dependencies: sax ">=0.6.0" - xmlbuilder "~9.0.1" + xmlbuilder "~11.0.0" -xmlbuilder@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58" - integrity sha1-xGFLp04K0ZbmCcknLNnh3bKKilg= +xmlbuilder@>=11.0.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= -xmlbuilder@~9.0.1: - version "9.0.4" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" - integrity sha1-UZy0ymhtAFqEINNJbz8MruzKWA8= +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== xmldom@0.1.x: version "0.1.31" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== -xtend@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -xtend@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= - -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.3.0: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - yauzl@^2.3.1: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" @@ -3794,12 +1962,17 @@ yauzl@^2.3.1: fd-slicer "~1.1.0" yazl@^2.2.2: - version "2.4.3" - resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071" - integrity sha1-7CblzIfVYBud+EMtvdPNLlFzoHE= + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== dependencies: buffer-crc32 "~0.2.3" +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zone.js@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" diff --git a/cgmanifest.json b/cgmanifest.json index 590f0b59c1..f9aa3cd5fb 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "894fb9eb56c6cbda65e3c3ae9ada6d4cb5850cc9" + "commitHash": "c0dfcf99c0bbc9c4763c70e5034eb1a970a9ff3b" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "83.0.4103.122" + "version": "87.0.4280.141" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "9622fed3fb2cffcea9efff6c8cb4cc2def99d75d" + "commitHash": "e3e0927bb93ed92bcdfe81e7ad9af3d78ccc74fb" } }, "isOnlyProductionDependency": true, - "version": "12.14.1" + "version": "12.18.3" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "ca82414364002efa665ffa7427e267adf76ed1f3" + "commitHash": "805e442ff873e10735a1ea18021f491597afa885" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "9.4.3" + "version": "11.2.2" }, { "component": { @@ -132,11 +132,11 @@ "git": { "name": "vscode-codicons", "repositoryUrl": "https://github.com/microsoft/vscode-codicons", - "commitHash": "f0caa623812a8ed5059516277675b4158d4c4867" + "commitHash": "ccdcf91d57d3a5a1d6b620d95d518bab4d75984d" } }, "license": "MIT and Creative Commons Attribution 4.0", - "version": "0.0.1" + "version": "0.0.14" }, { "component": { diff --git a/extensions/bat/package.json b/extensions/bat/package.json index dc543aef40..5d36ae73ab 100644 --- a/extensions/bat/package.json +++ b/extensions/bat/package.json @@ -1,29 +1,47 @@ { - "name": "bat", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js mmims/language-batchfile grammars/batchfile.cson ./syntaxes/batchfile.tmLanguage.json" - }, - "contributes": { - "languages": [{ - "id": "bat", - "extensions": [ ".bat", ".cmd"], - "aliases": [ "Batch", "bat" ], - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "bat", - "scopeName": "source.batchfile", - "path": "./syntaxes/batchfile.tmLanguage.json" - }], - "snippets": [{ - "language": "bat", - "path": "./snippets/batchfile.code-snippets" - }] - } + "name": "bat", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.52.0" + }, + "scripts": { + "update-grammar": "node ../node_modules/.bin/vscode-grammar-updater mmims/language-batchfile grammars/batchfile.cson ./syntaxes/batchfile.tmLanguage.json" + }, + "contributes": { + "languages": [ + { + "id": "bat", + "extensions": [ + ".bat", + ".cmd" + ], + "aliases": [ + "Batch", + "bat" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "bat", + "scopeName": "source.batchfile", + "path": "./syntaxes/batchfile.tmLanguage.json" + } + ], + "snippets": [ + { + "language": "bat", + "path": "./snippets/batchfile.code-snippets" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/bat/package.nls.json b/extensions/bat/package.nls.json index c5052ca021..00a69b39d4 100644 --- a/extensions/bat/package.nls.json +++ b/extensions/bat/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Windows Bat Language Basics", "description": "Provides snippets, syntax highlighting, bracket matching and folding in Windows batch files." -} \ No newline at end of file +} diff --git a/extensions/bat/yarn.lock b/extensions/bat/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/bat/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/big-data-cluster/.vscodeignore b/extensions/big-data-cluster/.vscodeignore index 09ae13c3bb..84f994a5d2 100644 --- a/extensions/big-data-cluster/.vscodeignore +++ b/extensions/big-data-cluster/.vscodeignore @@ -7,8 +7,8 @@ tsconfig.json yarn.lock node_modules -!node_modules/ads-kerberos/package.json -!node_modules/ads-kerberos/LICENSE -!node_modules/ads-kerberos/lib -!node_modules/ads-kerberos/index.js -!node_modules/ads-kerberos/build/Release/kerberos.node +!node_modules/ads-kerberos2/package.json +!node_modules/ads-kerberos2/LICENSE +!node_modules/ads-kerberos2/lib +!node_modules/ads-kerberos2/index.js +!node_modules/ads-kerberos2/build/Release/kerberos.node diff --git a/extensions/big-data-cluster/extension.webpack.config.js b/extensions/big-data-cluster/extension.webpack.config.js index 7a3c51786e..02ba06bfc1 100644 --- a/extensions/big-data-cluster/extension.webpack.config.js +++ b/extensions/big-data-cluster/extension.webpack.config.js @@ -15,6 +15,6 @@ module.exports = withDefaults({ extension: './src/extension.ts' }, externals: { - 'ads-kerberos': 'commonjs ads-kerberos' + 'ads-kerberos2': 'commonjs ads-kerberos2' } }); diff --git a/extensions/big-data-cluster/package.json b/extensions/big-data-cluster/package.json index 3d87a097f9..67edfa26f1 100644 --- a/extensions/big-data-cluster/package.json +++ b/extensions/big-data-cluster/package.json @@ -350,7 +350,7 @@ ] }, "dependencies": { - "ads-kerberos": "^1.1.3", + "ads-kerberos2": "^1.1.3", "request": "^2.88.0", "vscode-nls": "^4.0.0" }, diff --git a/extensions/big-data-cluster/src/bigDataCluster/auth.ts b/extensions/big-data-cluster/src/bigDataCluster/auth.ts index 664c736453..90e43f05db 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/auth.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/auth.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as kerberos from 'ads-kerberos'; +import * as kerberos from 'ads-kerberos2'; import * as vscode from 'vscode'; export async function authenticateKerberos(hostname: string): Promise { diff --git a/extensions/big-data-cluster/yarn.lock b/extensions/big-data-cluster/yarn.lock index 3476d56f95..e9fa74ba92 100644 --- a/extensions/big-data-cluster/yarn.lock +++ b/extensions/big-data-cluster/yarn.lock @@ -27,10 +27,10 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d" integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg== -ads-kerberos@^1.1.3: +ads-kerberos2@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/ads-kerberos/-/ads-kerberos-1.1.3.tgz#e424253c6f0df2b258aa4a31804f9e38562d0301" - integrity sha512-e6rLQ/ECDd+vo7FhcjbEZPxH3I9cDc/zBj0ih0CH//zZw/a4HI0g/buIuTzU7yRMC5FYEEH9azC82vO6OTbu8g== + resolved "https://registry.yarnpkg.com/ads-kerberos2/-/ads-kerberos2-1.1.3.tgz#2f52c228579c45e7ea975c64070f33d2fdfafb58" + integrity sha512-mSHGHcVuST50I7nekS2gPH9mDDkeoZQagP2mit8Ry2EhAFxXXw/px1wESPe+M60SzSHbXaJ+orV5izoB2Mee9w== dependencies: nan "^2.14.0" diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 4234004132..6d99ab0d5b 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -128,6 +128,10 @@ ] }, "devDependencies": { - "@types/node": "^12.11.7" + "@types/node": "^12.19.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/configuration-editing/package.nls.json b/extensions/configuration-editing/package.nls.json index 20a9c1af8b..860ed4915c 100644 --- a/extensions/configuration-editing/package.nls.json +++ b/extensions/configuration-editing/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Configuration Editing", "description": "Provides capabilities (advanced IntelliSense, auto-fixing) in configuration files like settings, launch, and extension recommendation files." -} \ No newline at end of file +} diff --git a/extensions/configuration-editing/src/configurationEditingMain.ts b/extensions/configuration-editing/src/configurationEditingMain.ts index 20d34ef153..c8ec878580 100644 --- a/extensions/configuration-editing/src/configurationEditingMain.ts +++ b/extensions/configuration-editing/src/configurationEditingMain.ts @@ -81,7 +81,7 @@ function registerExtensionsCompletionsInExtensionsDocument(): vscode.Disposable const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'recommendations') { const extensionsContent = parse(document.getText()); - return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } @@ -95,7 +95,7 @@ function registerExtensionsCompletionsInWorkspaceConfigurationDocument(): vscode const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'extensions' && location.path[1] === 'recommendations') { const extensionsContent = parse(document.getText())['extensions']; - return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } diff --git a/extensions/configuration-editing/src/extensionsProposals.ts b/extensions/configuration-editing/src/extensionsProposals.ts index f447303f85..3d7e68e289 100644 --- a/extensions/configuration-editing/src/extensionsProposals.ts +++ b/extensions/configuration-editing/src/extensionsProposals.ts @@ -8,14 +8,14 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -export function provideInstalledExtensionProposals(existing: string[], range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { +export function provideInstalledExtensionProposals(existing: string[], additionalText: string, range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { if (Array.isArray(existing)) { const extensions = includeBuiltinExtensions ? vscode.extensions.all : vscode.extensions.all.filter(e => !(e.id.startsWith('vscode.') || e.id === 'Microsoft.vscode-markdown')); const knownExtensionProposals = extensions.filter(e => existing.indexOf(e.id) === -1); if (knownExtensionProposals.length) { return knownExtensionProposals.map(e => { const item = new vscode.CompletionItem(e.id); - const insertText = `"${e.id}"`; + const insertText = `"${e.id}"${additionalText}`; item.kind = vscode.CompletionItemKind.Value; item.insertText = insertText; item.range = range; diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index a39a0136af..d063bfb096 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -48,7 +48,16 @@ export class SettingsDocument { try { ignoredExtensions = parse(this.document.getText())['settingsSync.ignoredExtensions']; } catch (e) {/* ignore error */ } - return provideInstalledExtensionProposals(ignoredExtensions, range, true); + return provideInstalledExtensionProposals(ignoredExtensions, '', range, true); + } + + // remote.extensionKind + if (location.path[0] === 'remote.extensionKind' && location.path.length === 2 && location.isAtPropertyKey) { + let alreadyConfigured: string[] = []; + try { + alreadyConfigured = Object.keys(parse(this.document.getText())['remote.extensionKind']); + } catch (e) {/* ignore error */ } + return provideInstalledExtensionProposals(alreadyConfigured, `: [\n\t"ui"\n]`, range, true); } return this.provideLanguageOverridesCompletionItems(location, position); diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 36aab5fd22..ebfaa046da 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== jsonc-parser@^2.2.1: version "2.2.1" diff --git a/extensions/dacpac/src/test/testContext.ts b/extensions/dacpac/src/test/testContext.ts index 0d939e584d..f8622819c1 100644 --- a/extensions/dacpac/src/test/testContext.ts +++ b/extensions/dacpac/src/test/testContext.ts @@ -38,7 +38,8 @@ export function createContext(): TestContext { extensionMode: undefined as any, globalStorageUri: undefined, logUri: undefined, - storageUri: undefined + storageUri: undefined, + secrets: undefined }, viewContext: viewContext }; diff --git a/extensions/docker/package.json b/extensions/docker/package.json index a1cc782d21..9d36db0825 100644 --- a/extensions/docker/package.json +++ b/extensions/docker/package.json @@ -1,34 +1,57 @@ { - "name": "docker", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js moby/moby contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage ./syntaxes/docker.tmLanguage.json" - }, - "contributes": { - "languages": [{ - "id": "dockerfile", - "extensions": [ ".dockerfile", ".containerfile" ], - "filenames": [ "Dockerfile", "Containerfile" ], - "filenamePatterns": [ "Dockerfile.*", "Containerfile.*" ], - "aliases": [ "Docker", "Dockerfile", "Containerfile" ], - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "dockerfile", - "scopeName": "source.dockerfile", - "path": "./syntaxes/docker.tmLanguage.json" - }], - "configurationDefaults": { - "[dockerfile]": { - "editor.quickSuggestions": { - "strings": true - } - } - } - } + "name": "docker", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "scripts": { + "update-grammar": "node ../node_modules/.bin/vscode-grammar-updater moby/moby contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage ./syntaxes/docker.tmLanguage.json" + }, + "contributes": { + "languages": [ + { + "id": "dockerfile", + "extensions": [ + ".dockerfile", + ".containerfile" + ], + "filenames": [ + "Dockerfile", + "Containerfile" + ], + "filenamePatterns": [ + "Dockerfile.*", + "Containerfile.*" + ], + "aliases": [ + "Docker", + "Dockerfile", + "Containerfile" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "dockerfile", + "scopeName": "source.dockerfile", + "path": "./syntaxes/docker.tmLanguage.json" + } + ], + "configurationDefaults": { + "[dockerfile]": { + "editor.quickSuggestions": { + "strings": true + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/docker/package.nls.json b/extensions/docker/package.nls.json index 2a38ca3da6..597ed85fbe 100644 --- a/extensions/docker/package.nls.json +++ b/extensions/docker/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Docker Language Basics", "description": "Provides syntax highlighting and bracket matching in Docker files." -} \ No newline at end of file +} diff --git a/extensions/docker/yarn.lock b/extensions/docker/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/docker/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index bdd02d870a..c520abb226 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -1,64 +1,71 @@ { - "name": "extension-editing", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { - "vscode": "^1.4.0" - }, - "activationEvents": [ - "onLanguage:json", - "onLanguage:markdown", - "onLanguage:typescript" - ], - "main": "./out/extensionEditingMain", - "browser": "./dist/browser/extensionEditingBrowserMain", - "scripts": { - "compile": "gulp compile-extension:extension-editing", - "watch": "gulp watch-extension:extension-editing" - }, - "dependencies": { - "jsonc-parser": "^2.2.1", - "markdown-it": "^8.3.1", - "parse5": "^3.0.2", - "vscode-nls": "^4.1.1" - }, - "contributes": { - "jsonValidation": [ - { - "fileMatch": "package.json", - "url": "vscode://schemas/vscode-extensions" - }, - { - "fileMatch": "*language-configuration.json", - "url": "vscode://schemas/language-configuration" - }, - { - "fileMatch": ["*icon-theme.json", "!*product-icon-theme.json"], - "url": "vscode://schemas/icon-theme" - }, - { - "fileMatch": "*product-icon-theme.json", - "url": "vscode://schemas/product-icon-theme" - }, - { - "fileMatch": "*color-theme.json", - "url": "vscode://schemas/color-theme" - } - ], - "languages": [ - { - "id": "ignore", - "filenames": [ - ".vscodeignore" - ] - } - ] - }, - "devDependencies": { - "@types/markdown-it": "0.0.2", - "@types/node": "^12.11.7" - } + "name": "extension-editing", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.4.0" + }, + "activationEvents": [ + "onLanguage:json", + "onLanguage:markdown", + "onLanguage:typescript" + ], + "main": "./out/extensionEditingMain", + "browser": "./dist/browser/extensionEditingBrowserMain", + "scripts": { + "compile": "gulp compile-extension:extension-editing", + "watch": "gulp watch-extension:extension-editing" + }, + "dependencies": { + "jsonc-parser": "^2.2.1", + "markdown-it": "^8.3.1", + "parse5": "^3.0.2", + "vscode-nls": "^4.1.1" + }, + "contributes": { + "jsonValidation": [ + { + "fileMatch": "package.json", + "url": "vscode://schemas/vscode-extensions" + }, + { + "fileMatch": "*language-configuration.json", + "url": "vscode://schemas/language-configuration" + }, + { + "fileMatch": [ + "*icon-theme.json", + "!*product-icon-theme.json" + ], + "url": "vscode://schemas/icon-theme" + }, + { + "fileMatch": "*product-icon-theme.json", + "url": "vscode://schemas/product-icon-theme" + }, + { + "fileMatch": "*color-theme.json", + "url": "vscode://schemas/color-theme" + } + ], + "languages": [ + { + "id": "ignore", + "filenames": [ + ".vscodeignore" + ] + } + ] + }, + "devDependencies": { + "@types/markdown-it": "0.0.2", + "@types/node": "^12.19.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/extension-editing/package.nls.json b/extensions/extension-editing/package.nls.json index 263fb6bc87..1801b0908d 100644 --- a/extensions/extension-editing/package.nls.json +++ b/extensions/extension-editing/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Extension Authoring", "description": "Provides linting capabilities for authoring extensions." -} \ No newline at end of file +} diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 50adf31c09..0cc5235479 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== "@types/node@^6.0.46": version "6.0.78" diff --git a/extensions/git-ui/package.json b/extensions/git-ui/package.json index b08942888e..67c6c940fa 100644 --- a/extensions/git-ui/package.json +++ b/extensions/git-ui/package.json @@ -3,6 +3,7 @@ "displayName": "%displayName%", "description": "%description%", "publisher": "vscode", + "license": "MIT", "version": "1.0.0", "engines": { "vscode": "^1.5.0" @@ -25,6 +26,10 @@ "watch": "gulp watch-extension:git-ui" }, "devDependencies": { - "@types/node": "^12.11.7" + "@types/node": "^12.19.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/git-ui/package.nls.json b/extensions/git-ui/package.nls.json index 5303e91f4c..413d891f1f 100644 --- a/extensions/git-ui/package.nls.json +++ b/extensions/git-ui/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Git UI", "description": "Git SCM UI Integration" -} \ No newline at end of file +} diff --git a/extensions/git-ui/yarn.lock b/extensions/git-ui/yarn.lock index 40784952b8..e03bdd573e 100644 --- a/extensions/git-ui/yarn.lock +++ b/extensions/git-ui/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== diff --git a/extensions/git/build/update-grammars.js b/extensions/git/build/update-grammars.js index 2aa490561e..5d91821c2b 100644 --- a/extensions/git/build/update-grammars.js +++ b/extensions/git/build/update-grammars.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -var updateGrammar = require('../../../build/npm/update-grammar'); +var updateGrammar = require('vscode-grammar-updater'); updateGrammar.update('textmate/git.tmbundle', 'Syntaxes/Git%20Commit%20Message.tmLanguage', './syntaxes/git-commit.tmLanguage.json'); updateGrammar.update('textmate/git.tmbundle', 'Syntaxes/Git%20Rebase%20Message.tmLanguage', './syntaxes/git-rebase.tmLanguage.json'); diff --git a/extensions/git/package.json b/extensions/git/package.json index eb40121f3a..b84fb2b0f9 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -24,7 +24,7 @@ "watch": "gulp watch-extension:git", "update-emoji": "node ./build/update-emoji.js", "update-grammar": "node ./build/update-grammars.js", - "test": "mocha" + "test": "node ../../node_modules/mocha/bin/mocha" }, "contributes": { "commands": [ @@ -71,6 +71,11 @@ "category": "Git", "icon": "$(compare-changes)" }, + { + "command": "git.openAllChanges", + "title": "%command.openAllChanges%", + "category": "Git" + }, { "command": "git.openFile", "title": "%command.openFile%", @@ -1287,6 +1292,11 @@ "group": "navigation", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" }, + { + "command": "git.openFile", + "group": "navigation", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInNotebookTextDiffEditor && resourceScheme =~ /^git$|^file$/" + }, { "command": "git.openChange", "group": "navigation", @@ -1644,7 +1654,17 @@ "default": true }, "git.autofetch": { - "type": "boolean", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "all" + ] + } + ], "scope": "resource", "description": "%config.autofetch%", "default": false, @@ -1732,6 +1752,11 @@ "description": "%config.ignoreLimitWarning%", "default": false }, + "git.ignoreRebaseWarning": { + "type": "boolean", + "description": "%config.ignoreRebaseWarning%", + "default": false + }, "git.defaultCloneDirectory": { "type": [ "string", @@ -2040,6 +2065,11 @@ "description": "%config.untrackedChanges%", "scope": "resource" }, + "git.requireGitUserConfig": { + "type": "boolean", + "description": "%config.requireGitUserConfig%", + "default": true + }, "git.showCommitInput": { "type": "boolean", "scope": "resource", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 416161e811..1d723cd10f 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -9,6 +9,7 @@ "command.close": "Close Repository", "command.refresh": "Refresh", "command.openChange": "Open Changes", + "command.openAllChanges": "Open All Changes", "command.openFile": "Open File", "command.openHEADFile": "Open File (HEAD)", "command.stage": "Stage Changes", @@ -97,7 +98,7 @@ "config.autoRepositoryDetection.subFolders": "Scan for subfolders of the currently opened folder.", "config.autoRepositoryDetection.openEditors": "Scan for parent folders of open files.", "config.autorefresh": "Whether auto refreshing is enabled.", - "config.autofetch": "When enabled, commits will automatically be fetched from the default remote of the current Git repository.", + "config.autofetch": "When set to true, commits will automatically be fetched from the default remote of the current Git repository. Setting to `all` will fetch from all remotes", "config.autofetchPeriod": "Duration in seconds between each automatic git fetch, when `git.autofetch` is enabled.", "config.confirmSync": "Confirm before synchronizing git repositories.", "config.countBadge": "Controls the Git count badge.", @@ -114,6 +115,7 @@ "config.ignoreMissingGitWarning": "Ignores the warning when Git is missing.", "config.ignoreWindowsGit27Warning": "Ignores the warning when Git 2.25 - 2.26 is installed on Windows.", "config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.", + "config.ignoreRebaseWarning": "Ignores the warning when it looks like the branch might have been rebased when pulling.", "config.defaultCloneDirectory": "The default location to clone a git repository.", "config.enableSmartCommit": "Commit all changes when there are no staged changes.", "config.smartCommitChanges": "Control which changes are automatically staged by Smart Commit.", @@ -173,6 +175,7 @@ "config.untrackedChanges.mixed": "All changes, tracked and untracked, appear together and behave equally.", "config.untrackedChanges.separate": "Untracked changes appear separately in the Source Control view. They are also excluded from several actions.", "config.untrackedChanges.hidden": "Untracked changes are hidden and excluded from several actions.", + "config.requireGitUserConfig": "Controls whether to require explicit Git user configuration or allow Git to guess if missing", "config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.", "config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for git processes spawned in the integrated terminal. Note: terminals need to be restarted to pick up a change in this setting.", "config.timeline.showAuthor": "Controls whether to show the commit author in the Timeline view", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index d6fe1eb835..be91c429b2 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -5,7 +5,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery, PushErrorHandler } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode'; import { mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -201,8 +201,8 @@ export class ApiRepository implements Repository { return this._repository.pull(undefined, unshallow); } - push(remoteName?: string, branchName?: string, setUpstream: boolean = false): Promise { - return this._repository.pushTo(remoteName, branchName, setUpstream); + push(remoteName?: string, branchName?: string, setUpstream: boolean = false, force?: ForcePushMode): Promise { + return this._repository.pushTo(remoteName, branchName, setUpstream, force); } blame(path: string): Promise { @@ -237,6 +237,10 @@ export class ApiImpl implements API { return this._model.onDidChangeState; } + get onDidPublish(): Event { + return this._model.onDidPublish; + } + get onDidOpenRepository(): Event { return mapEvent(this._model.onDidOpenRepository, r => new ApiRepository(r)); } @@ -265,6 +269,11 @@ export class ApiImpl implements API { return this.getRepository(root) || null; } + async openRepository(root: Uri): Promise { + await this._model.openRepository(root.fsPath); + return this.getRepository(root) || null; + } + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index b4322d4a7f..547bf7d132 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -14,6 +14,11 @@ export interface InputBox { value: string; } +export const enum ForcePushMode { + Force, + ForceWithLease +} + export const enum RefType { Head, RemoteHead, @@ -131,6 +136,7 @@ export interface CommitOptions { signCommit?: boolean; empty?: boolean; noVerify?: boolean; + requireUserConfig?: boolean; } export interface BranchQuery { @@ -193,7 +199,7 @@ export interface Repository { fetch(remote?: string, ref?: string, depth?: number): Promise; pull(unshallow?: boolean): Promise; - push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise; + push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise; blame(path: string): Promise; log(options?: LogOptions): Promise; @@ -231,9 +237,15 @@ export interface PushErrorHandler { export type APIState = 'uninitialized' | 'initialized'; +export interface PublishEvent { + repository: Repository; + branch?: string; +} + export interface API { readonly state: APIState; readonly onDidChangeState: Event; + readonly onDidPublish: Event; readonly git: Git; readonly repositories: Repository[]; readonly onDidOpenRepository: Event; @@ -242,6 +254,7 @@ export interface API { toGitUri(uri: Uri, ref: string): Uri; getRepository(uri: Uri): Repository | null; init(root: Uri): Promise; + openRepository(root: Uri): Promise registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; registerCredentialsProvider(provider: CredentialsProvider): Disposable; diff --git a/extensions/git/src/askpass-main.ts b/extensions/git/src/askpass-main.ts index 2da27b1586..7093c5b0e6 100644 --- a/extensions/git/src/askpass-main.ts +++ b/extensions/git/src/askpass-main.ts @@ -30,7 +30,7 @@ function main(argv: string[]): void { const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string; const request = argv[2]; - const host = argv[4].substring(1, argv[4].length - 2); + const host = argv[4].replace(/^["']+|["']+$/g, ''); const ipcClient = new IPCClient('askpass'); ipcClient.call({ request, host }).then(res => { diff --git a/extensions/git/src/autofetch.ts b/extensions/git/src/autofetch.ts index f33685868a..fc73e3952f 100644 --- a/extensions/git/src/autofetch.ts +++ b/extensions/git/src/autofetch.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri } from 'vscode'; +import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent } from 'vscode'; import { Repository, Operation } from './repository'; import { eventToPromise, filterEvent, onceEvent } from './util'; import * as nls from 'vscode-nls'; @@ -23,6 +23,7 @@ export class AutoFetcher { private onDidChange = this._onDidChange.event; private _enabled: boolean = false; + private _fetchAll: boolean = false; get enabled(): boolean { return this._enabled; } set enabled(enabled: boolean) { this._enabled = enabled; this._onDidChange.fire(enabled); } @@ -68,13 +69,26 @@ export class AutoFetcher { this.globalState.update(AutoFetcher.DidInformUser, true); } - private onConfiguration(): void { - const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root)); + private onConfiguration(e?: ConfigurationChangeEvent): void { + if (e !== undefined && !e.affectsConfiguration('git.autofetch')) { + return; + } - if (gitConfig.get('autofetch') === false) { - this.disable(); - } else { - this.enable(); + const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root)); + switch (gitConfig.get('autofetch')) { + case true: + this._fetchAll = false; + this.enable(); + break; + case 'all': + this._fetchAll = true; + this.enable(); + break; + case false: + default: + this._fetchAll = false; + this.disable(); + break; } } @@ -100,7 +114,11 @@ export class AutoFetcher { } try { - await this.repository.fetchDefault({ silent: true }); + if (this._fetchAll) { + await this.repository.fetchAll(); + } else { + await this.repository.fetchDefault({ silent: true }); + } } catch (err) { if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) { this.disable(); diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 702d0632f4..b92a9bedab 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -8,8 +8,8 @@ import * as path from 'path'; import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; -import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git'; -import { ForcePushMode, Git, Stash } from './git'; +import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git'; +import { Git, Stash } from './git'; import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; @@ -54,7 +54,7 @@ class CheckoutRemoteHeadItem extends CheckoutItem { return localize('remote branch at', "Remote branch at {0}", this.shortCommit); } - async run(repository: Repository): Promise { + async run(repository: Repository, opts?: { detached?: boolean }): Promise { if (!this.ref.name) { return; } @@ -62,9 +62,9 @@ class CheckoutRemoteHeadItem extends CheckoutItem { const branches = await repository.findTrackingBranches(this.ref.name); if (branches.length > 0) { - await repository.checkout(branches[0].name!); + await repository.checkout(branches[0].name!, opts); } else { - await repository.checkoutTracking(this.ref.name); + await repository.checkoutTracking(this.ref.name, opts); } } } @@ -277,6 +277,12 @@ interface PushOptions { pushType: PushType; forcePush?: boolean; silent?: boolean; + + pushTo?: { + remote?: string; + refspec?: string; + setUpstream?: boolean; + } } class CommandErrorOutputTextDocumentContentProvider implements TextDocumentContentProvider { @@ -366,6 +372,24 @@ export class CommandCenter { await resource.open(); } + @command('git.openAllChanges', { repository: true }) + async openChanges(repository: Repository): Promise { + for (const resource of [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]) { + if ( + resource.type === Status.DELETED || resource.type === Status.DELETED_BY_THEM || + resource.type === Status.DELETED_BY_US || resource.type === Status.BOTH_DELETED + ) { + continue; + } + + void commands.executeCommand( + 'vscode.open', + resource.resourceUri, + { background: true, preview: false, } + ); + } + } + async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise { if (!url || typeof url !== 'string') { url = await pickRemoteSource(this.model, { @@ -500,12 +524,12 @@ export class CommandCenter { @command('git.clone') async clone(url?: string, parentPath?: string): Promise { - this.cloneRepository(url, parentPath); + await this.cloneRepository(url, parentPath); } @command('git.cloneRecursive') async cloneRecursive(url?: string, parentPath?: string): Promise { - this.cloneRepository(url, parentPath, { recursive: true }); + await this.cloneRepository(url, parentPath, { recursive: true }); } @command('git.init') @@ -1323,8 +1347,8 @@ export class CommandCenter { const enableSmartCommit = config.get('enableSmartCommit') === true; const enableCommitSigning = config.get('enableCommitSigning') === true; - const noStagedChanges = repository.indexGroup.resourceStates.length === 0; - const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; + let noStagedChanges = repository.indexGroup.resourceStates.length === 0; + let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; if (promptToSaveFilesBeforeCommit !== 'never') { let documents = workspace.textDocuments @@ -1346,6 +1370,9 @@ export class CommandCenter { if (pick === saveAndCommit) { await Promise.all(documents.map(d => d.save())); await repository.add(documents.map(d => d.uri)); + + noStagedChanges = repository.indexGroup.resourceStates.length === 0; + noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; } else if (pick !== commit) { return false; // do not commit on cancel } @@ -2113,23 +2140,27 @@ export class CommandCenter { } } else { const branchName = repository.HEAD.name; - const addRemote = new AddRemoteItem(this); - const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote]; - const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName); - const choice = await window.showQuickPick(picks, { placeHolder }); + if (!pushOptions.pushTo?.remote) { + const addRemote = new AddRemoteItem(this); + const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote]; + const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName); + const choice = await window.showQuickPick(picks, { placeHolder }); - if (!choice) { - return; - } + if (!choice) { + return; + } - if (choice === addRemote) { - const newRemote = await this.addRemote(repository); + if (choice === addRemote) { + const newRemote = await this.addRemote(repository); - if (newRemote) { - await repository.pushTo(newRemote, branchName, undefined, forcePushMode); + if (newRemote) { + await repository.pushTo(newRemote, branchName, undefined, forcePushMode); + } + } else { + await repository.pushTo(choice.label, branchName, undefined, forcePushMode); } } else { - await repository.pushTo(choice.label, branchName, undefined, forcePushMode); + await repository.pushTo(pushOptions.pushTo.remote, pushOptions.pushTo.refspec || branchName, pushOptions.pushTo.setUpstream, forcePushMode); } } } @@ -2170,13 +2201,13 @@ export class CommandCenter { } @command('git.pushTo', { repository: true }) - async pushTo(repository: Repository): Promise { - await this._push(repository, { pushType: PushType.PushTo }); + async pushTo(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise { + await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream } }); } @command('git.pushToForce', { repository: true }) - async pushToForce(repository: Repository): Promise { - await this._push(repository, { pushType: PushType.PushTo, forcePush: true }); + async pushToForce(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise { + await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream }, forcePush: true }); } @command('git.pushTags', { repository: true }) @@ -2356,11 +2387,16 @@ export class CommandCenter { } await provider.publishRepository!(new ApiRepository(repository)); + this.model.firePublishEvent(repository, branchName); + return; } if (remotes.length === 1) { - return await repository.pushTo(remotes[0].name, branchName, true); + await repository.pushTo(remotes[0].name, branchName, true); + this.model.firePublishEvent(repository, branchName); + + return; } const addRemote = new AddRemoteItem(this); @@ -2377,9 +2413,13 @@ export class CommandCenter { if (newRemote) { await repository.pushTo(newRemote, branchName, true); + + this.model.firePublishEvent(repository, branchName); } } else { await repository.pushTo(choice.label, branchName, true); + + this.model.firePublishEvent(repository, branchName); } } @@ -2649,7 +2689,7 @@ export class CommandCenter { return Promise.resolve(); } - return Promise.resolve(method.apply(this, [repository, ...args])); + return Promise.resolve(method.apply(this, [repository, ...args.slice(1)])); }); } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 75faae4647..60ca0d70b4 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -14,7 +14,7 @@ import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util'; import { CancellationToken, Progress, Uri } from 'vscode'; import { detectEncoding } from './encoding'; -import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery } from './api/git'; +import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -311,6 +311,7 @@ export class GitError { export interface IGitOptions { gitPath: string; + userAgent: string; version: string; env?: any; } @@ -362,6 +363,8 @@ export interface ICloneOptions { export class Git { readonly path: string; + readonly userAgent: string; + readonly version: string; private env: any; private _onOutput = new EventEmitter(); @@ -369,6 +372,8 @@ export class Git { constructor(options: IGitOptions) { this.path = options.gitPath; + this.version = options.version; + this.userAgent = options.userAgent; this.env = options.env || {}; } @@ -427,7 +432,11 @@ export class Git { if (options.recursive) { command.push('--recursive'); } - await this.exec(options.parentPath, command, { cancellationToken, onSpawn }); + await this.exec(options.parentPath, command, { + cancellationToken, + env: { 'GIT_HTTP_USER_AGENT': this.userAgent }, + onSpawn, + }); } catch (err) { if (err.stderr) { err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim(); @@ -458,7 +467,7 @@ export class Git { try { const networkPath = await new Promise(resolve => - realpath.native(`${letter}:`, { encoding: 'utf8' }, (err, resolvedPath) => + realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) => resolve(err !== null ? undefined : resolvedPath), ), ); @@ -798,11 +807,6 @@ export interface PullOptions { readonly cancellationToken?: CancellationToken; } -export enum ForcePushMode { - Force, - ForceWithLease -} - export class Repository { constructor( @@ -819,8 +823,7 @@ export class Repository { return this.repositoryRoot; } - // TODO@Joao: rename to exec - async run(args: string[], options: SpawnOptions = {}): Promise> { + async exec(args: string[], options: SpawnOptions = {}): Promise> { return await this.git.exec(this.repositoryRoot, args, options); } @@ -845,7 +848,7 @@ export class Repository { args.push(value); } - const result = await this.run(args, options); + const result = await this.exec(args, options); return result.stdout.trim(); } @@ -858,7 +861,7 @@ export class Repository { args.push('-l'); - const result = await this.run(args); + const result = await this.exec(args); const lines = result.stdout.trim().split(/\r|\r\n|\n/); return lines.map(entry => { @@ -874,7 +877,7 @@ export class Repository { args.push(options.path); } - const result = await this.run(args); + const result = await this.exec(args); if (result.exitCode) { // An empty repo return []; @@ -905,7 +908,7 @@ export class Repository { args.push('--', uri.fsPath); - const result = await this.run(args); + const result = await this.exec(args); if (result.exitCode) { // No file history, e.g. a new file or untracked return []; @@ -960,7 +963,7 @@ export class Repository { } const { mode, object } = elements[0]; - const catFile = await this.run(['cat-file', '-s', object]); + const catFile = await this.exec(['cat-file', '-s', object]); const size = parseInt(catFile.stdout); return { mode, object, size }; @@ -977,12 +980,12 @@ export class Repository { } async lstree(treeish: string, path: string): Promise { - const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', sanitizePath(path)]); + const { stdout } = await this.exec(['ls-tree', '-l', treeish, '--', sanitizePath(path)]); return parseLsTree(stdout); } async lsfiles(path: string): Promise { - const { stdout } = await this.run(['ls-files', '--stage', '--', sanitizePath(path)]); + const { stdout } = await this.exec(['ls-files', '--stage', '--', sanitizePath(path)]); return parseLsFiles(stdout); } @@ -1047,7 +1050,7 @@ export class Repository { } try { - await this.run(args); + await this.exec(args); } catch (err) { if (/patch does not apply/.test(err.stderr)) { err.gitErrorCode = GitErrorCodes.PatchDoesNotApply; @@ -1064,7 +1067,7 @@ export class Repository { args.push('--cached'); } - const result = await this.run(args); + const result = await this.exec(args); return result.stdout; } @@ -1077,7 +1080,7 @@ export class Repository { } const args = ['diff', '--', sanitizePath(path)]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout; } @@ -1090,7 +1093,7 @@ export class Repository { } const args = ['diff', ref, '--', sanitizePath(path)]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout; } @@ -1103,7 +1106,7 @@ export class Repository { } const args = ['diff', '--cached', '--', sanitizePath(path)]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout; } @@ -1116,13 +1119,13 @@ export class Repository { } const args = ['diff', '--cached', ref, '--', sanitizePath(path)]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout; } async diffBlobs(object1: string, object2: string): Promise { const args = ['diff', object1, object2]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout; } @@ -1136,7 +1139,7 @@ export class Repository { } const args = ['diff', range, '--', sanitizePath(path)]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout.trim(); } @@ -1151,7 +1154,7 @@ export class Repository { args.push(ref); } - const gitResult = await this.run(args); + const gitResult = await this.exec(args); if (gitResult.exitCode) { return []; } @@ -1224,14 +1227,14 @@ export class Repository { async getMergeBase(ref1: string, ref2: string): Promise { const args = ['merge-base', ref1, ref2]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout.trim(); } async hashObject(data: string): Promise { const args = ['hash-object', '-w', '--stdin']; - const result = await this.run(args, { input: data }); + const result = await this.exec(args, { input: data }); return result.stdout.trim(); } @@ -1247,10 +1250,10 @@ export class Repository { if (paths && paths.length) { for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { - await this.run([...args, '--', ...chunk]); + await this.exec([...args, '--', ...chunk]); } } else { - await this.run([...args, '--', '.']); + await this.exec([...args, '--', '.']); } } @@ -1263,7 +1266,7 @@ export class Repository { args.push(...paths.map(sanitizePath)); - await this.run(args); + await this.exec(args); } async stage(path: string, data: string): Promise { @@ -1296,7 +1299,7 @@ export class Repository { add = '--add'; } - await this.run(['update-index', add, '--cacheinfo', mode, hash, path]); + await this.exec(['update-index', add, '--cacheinfo', mode, hash, path]); } async checkout(treeish: string, paths: string[], opts: { track?: boolean, detached?: boolean } = Object.create(null)): Promise { @@ -1317,10 +1320,10 @@ export class Repository { try { if (paths && paths.length > 0) { for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { - await this.run([...args, '--', ...chunk]); + await this.exec([...args, '--', ...chunk]); } } else { - await this.run(args); + await this.exec(args); } } catch (err) { if (/Please,? commit your changes or stash them/.test(err.stderr || '')) { @@ -1365,25 +1368,27 @@ export class Repository { args.push('--no-verify'); } - // Stops git from guessing at user/email - args.splice(0, 0, '-c', 'user.useConfigOnly=true'); + if (opts.requireUserConfig ?? true) { + // Stops git from guessing at user/email + args.splice(0, 0, '-c', 'user.useConfigOnly=true'); + } try { - await this.run(args, !opts.amend || message ? { input: message || '' } : {}); + await this.exec(args, !opts.amend || message ? { input: message || '' } : {}); } catch (commitErr) { await this.handleCommitError(commitErr); } } async rebaseAbort(): Promise { - await this.run(['rebase', '--abort']); + await this.exec(['rebase', '--abort']); } async rebaseContinue(): Promise { const args = ['rebase', '--continue']; try { - await this.run(args); + await this.exec(args); } catch (commitErr) { await this.handleCommitError(commitErr); } @@ -1396,14 +1401,14 @@ export class Repository { } try { - await this.run(['config', '--get-all', 'user.name']); + await this.exec(['config', '--get-all', 'user.name']); } catch (err) { err.gitErrorCode = GitErrorCodes.NoUserNameConfigured; throw err; } try { - await this.run(['config', '--get-all', 'user.email']); + await this.exec(['config', '--get-all', 'user.email']); } catch (err) { err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured; throw err; @@ -1419,39 +1424,39 @@ export class Repository { args.push(ref); } - await this.run(args); + await this.exec(args); } async deleteBranch(name: string, force?: boolean): Promise { const args = ['branch', force ? '-D' : '-d', name]; - await this.run(args); + await this.exec(args); } async renameBranch(name: string): Promise { const args = ['branch', '-m', name]; - await this.run(args); + await this.exec(args); } async move(from: string, to: string): Promise { const args = ['mv', from, to]; - await this.run(args); + await this.exec(args); } async setBranchUpstream(name: string, upstream: string): Promise { const args = ['branch', '--set-upstream-to', upstream, name]; - await this.run(args); + await this.exec(args); } async deleteRef(ref: string): Promise { const args = ['update-ref', '-d', ref]; - await this.run(args); + await this.exec(args); } async merge(ref: string): Promise { const args = ['merge', ref]; try { - await this.run(args); + await this.exec(args); } catch (err) { if (/^CONFLICT /m.test(err.stdout || '')) { err.gitErrorCode = GitErrorCodes.Conflict; @@ -1470,12 +1475,12 @@ export class Repository { args = [...args, name]; } - await this.run(args); + await this.exec(args); } async deleteTag(name: string): Promise { let args = ['tag', '-d', name]; - await this.run(args); + await this.exec(args); } async clean(paths: string[]): Promise { @@ -1488,7 +1493,7 @@ export class Repository { for (const paths of groups) { for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { - promises.push(limiter.queue(() => this.run([...args, '--', ...chunk]))); + promises.push(limiter.queue(() => this.exec([...args, '--', ...chunk]))); } } @@ -1496,10 +1501,10 @@ export class Repository { } async undo(): Promise { - await this.run(['clean', '-fd']); + await this.exec(['clean', '-fd']); try { - await this.run(['checkout', '--', '.']); + await this.exec(['checkout', '--', '.']); } catch (err) { if (/did not match any file\(s\) known to git\./.test(err.stderr || '')) { return; @@ -1511,11 +1516,11 @@ export class Repository { async reset(treeish: string, hard: boolean = false): Promise { const args = ['reset', hard ? '--hard' : '--soft', treeish]; - await this.run(args); + await this.exec(args); } async revert(treeish: string, paths: string[]): Promise { - const result = await this.run(['branch']); + const result = await this.exec(['branch']); let args: string[]; // In case there are no branches, we must use rm --cached @@ -1528,10 +1533,10 @@ export class Repository { try { if (paths && paths.length > 0) { for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { - await this.run([...args, '--', ...chunk]); + await this.exec([...args, '--', ...chunk]); } } else { - await this.run([...args, '--', '.']); + await this.exec([...args, '--', '.']); } } catch (err) { // In case there are merge conflicts to be resolved, git reset will output @@ -1546,23 +1551,24 @@ export class Repository { async addRemote(name: string, url: string): Promise { const args = ['remote', 'add', name, url]; - await this.run(args); + await this.exec(args); } async removeRemote(name: string): Promise { const args = ['remote', 'remove', name]; - await this.run(args); + await this.exec(args); } async renameRemote(name: string, newName: string): Promise { const args = ['remote', 'rename', name, newName]; - await this.run(args); + await this.exec(args); } async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean, readonly cancellationToken?: CancellationToken } = {}): Promise { const args = ['fetch']; const spawnOptions: SpawnOptions = { cancellationToken: options.cancellationToken, + env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent } }; if (options.remote) { @@ -1584,11 +1590,11 @@ export class Repository { } if (options.silent) { - spawnOptions.env = { 'VSCODE_GIT_FETCH_SILENT': 'true' }; + spawnOptions.env!['VSCODE_GIT_FETCH_SILENT'] = 'true'; } try { - await this.run(args, spawnOptions); + await this.exec(args, spawnOptions); } catch (err) { if (/No remote repository specified\./.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified; @@ -1621,7 +1627,10 @@ export class Repository { } try { - await this.run(args, options); + await this.exec(args, { + cancellationToken: options.cancellationToken, + env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent } + }); } catch (err) { if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) { err.gitErrorCode = GitErrorCodes.Conflict; @@ -1648,7 +1657,7 @@ export class Repository { args.push(branch); try { - await this.run(args, options); + await this.exec(args, options); } catch (err) { if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) { err.gitErrorCode = GitErrorCodes.Conflict; @@ -1690,7 +1699,7 @@ export class Repository { } try { - await this.run(args); + await this.exec(args, { env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent } }); } catch (err) { if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.PushRejected; @@ -1708,13 +1717,13 @@ export class Repository { async cherryPick(commitHash: string): Promise { const args = ['cherry-pick', commitHash]; - await this.run(args); + await this.exec(args); } async blame(path: string): Promise { try { const args = ['blame', sanitizePath(path)]; - const result = await this.run(args); + const result = await this.exec(args); return result.stdout.trim(); } catch (err) { if (/^fatal: no such path/.test(err.stderr || '')) { @@ -1737,7 +1746,7 @@ export class Repository { args.push('-m', message); } - await this.run(args); + await this.exec(args); } catch (err) { if (/No local changes to save/.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.NoLocalChanges; @@ -1763,7 +1772,7 @@ export class Repository { args.push(`stash@{${index}}`); } - await this.run(args); + await this.exec(args); } catch (err) { if (/No stash found/.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.NoStashFound; @@ -1785,7 +1794,7 @@ export class Repository { } try { - await this.run(args); + await this.exec(args); } catch (err) { if (/No stash found/.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.NoStashFound; @@ -1850,7 +1859,7 @@ export class Repository { async getHEAD(): Promise { try { - const result = await this.run(['symbolic-ref', '--short', 'HEAD']); + const result = await this.exec(['symbolic-ref', '--short', 'HEAD']); if (!result.stdout) { throw new Error('Not in a branch'); @@ -1858,7 +1867,7 @@ export class Repository { return { name: result.stdout.trim(), commit: undefined, type: RefType.Head }; } catch (err) { - const result = await this.run(['rev-parse', 'HEAD']); + const result = await this.exec(['rev-parse', 'HEAD']); if (!result.stdout) { throw new Error('Error parsing HEAD'); @@ -1869,7 +1878,7 @@ export class Repository { } async findTrackingBranches(upstreamBranch: string): Promise { - const result = await this.run(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']); + const result = await this.exec(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']); return result.stdout.trim().split('\n') .map(line => line.trim().split('\0')) .filter(([_, upstream]) => upstream === upstreamBranch) @@ -1897,7 +1906,7 @@ export class Repository { args.push('--contains', opts.contains); } - const result = await this.run(args); + const result = await this.exec(args); const fn = (line: string): Ref | null => { let match: RegExpExecArray | null; @@ -1913,14 +1922,14 @@ export class Repository { return null; }; - return result.stdout.trim().split('\n') + return result.stdout.split('\n') .filter(line => !!line) .map(fn) .filter(ref => !!ref) as Ref[]; } async getStashes(): Promise { - const result = await this.run(['stash', 'list']); + const result = await this.exec(['stash', 'list']); const regex = /^stash@{(\d+)}:(.+)$/; const rawStashes = result.stdout.trim().split('\n') .filter(b => !!b) @@ -1932,7 +1941,7 @@ export class Repository { } async getRemotes(): Promise { - const result = await this.run(['remote', '--verbose']); + const result = await this.exec(['remote', '--verbose']); const lines = result.stdout.trim().split('\n').filter(l => !!l); const remotes: MutableRemote[] = []; @@ -1968,50 +1977,59 @@ export class Repository { return this.getHEAD(); } - let result = await this.run(['rev-parse', name]); - - if (!result.stdout && /^@/.test(name)) { - const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]); - name = symbolicFullNameResult.stdout.trim(); - - result = await this.run(['rev-parse', name]); + const args = ['for-each-ref', '--format=%(refname)%00%(upstream:short)%00%(upstream:track)%00%(objectname)']; + if (/^refs\/(head|remotes)\//i.test(name)) { + args.push(name); + } else { + args.push(`refs/heads/${name}`, `refs/remotes/${name}`); } - if (!result.stdout) { - return Promise.reject(new Error('No such branch')); - } + const result = await this.exec(args); + const branches: Branch[] = result.stdout.trim().split('\n').map(line => { + let [branchName, upstream, status, ref] = line.trim().split('\0'); - const commit = result.stdout.trim(); + if (branchName.startsWith('refs/heads/')) { + branchName = branchName.substring(11); + const index = upstream.indexOf('/'); - try { - const res2 = await this.run(['rev-parse', '--symbolic-full-name', name + '@{u}']); - const fullUpstream = res2.stdout.trim(); - const match = /^refs\/remotes\/([^/]+)\/(.+)$/.exec(fullUpstream); - - if (!match) { - throw new Error(`Could not parse upstream branch: ${fullUpstream}`); - } - - const upstream = { remote: match[1], name: match[2] }; - const res3 = await this.run(['rev-list', '--left-right', name + '...' + fullUpstream]); - - let ahead = 0, behind = 0; - let i = 0; - - while (i < res3.stdout.length) { - switch (res3.stdout.charAt(i)) { - case '<': ahead++; break; - case '>': behind++; break; - default: i++; break; + let ahead; + let behind; + const match = /\[(?:ahead ([0-9]+))?[,\s]*(?:behind ([0-9]+))?]|\[gone]/.exec(status); + if (match) { + [, ahead, behind] = match; } - while (res3.stdout.charAt(i++) !== '\n') { /* no-op */ } - } + return { + type: RefType.Head, + name: branchName, + upstream: upstream ? { + name: upstream.substring(index + 1), + remote: upstream.substring(0, index) + } : undefined, + commit: ref || undefined, + ahead: Number(ahead) || 0, + behind: Number(behind) || 0, + }; + } else if (branchName.startsWith('refs/remotes/')) { + branchName = branchName.substring(13); + const index = branchName.indexOf('/'); - return { name, type: RefType.Head, commit, upstream, ahead, behind }; - } catch (err) { - return { name, type: RefType.Head, commit }; + return { + type: RefType.RemoteHead, + name: branchName.substring(index + 1), + remote: branchName.substring(0, index), + commit: ref, + }; + } else { + return undefined; + } + }).filter((b?: Branch): b is Branch => !!b); + + if (branches.length) { + return branches[0]; } + + return Promise.reject(new Error('No such branch')); } async getBranches(query: BranchQuery): Promise { @@ -2048,7 +2066,7 @@ export class Repository { async getCommitTemplate(): Promise { try { - const result = await this.run(['config', '--get', 'commit.template']); + const result = await this.exec(['config', '--get', 'commit.template']); if (!result.stdout) { return ''; @@ -2071,7 +2089,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]); + const result = await this.exec(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]); const commits = parseGitCommits(result.stdout); if (commits.length === 0) { return Promise.reject('bad commit format'); @@ -2083,7 +2101,7 @@ export class Repository { const args = ['submodule', 'update']; for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { - await this.run([...args, '--', ...chunk]); + await this.exec([...args, '--', ...chunk]); } } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 7734c46cb8..20b1e544e7 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { ExtensionContext, workspace, window, Disposable, commands, OutputChannel, Uri } from 'vscode'; +import { ExtensionContext, workspace, window, Disposable, commands, Uri, OutputChannel } from 'vscode'; import { findGit, Git, IGit } from './git'; import { Model } from './model'; import { CommandCenter } from './commands'; @@ -20,6 +20,7 @@ import { GitProtocolHandler } from './protocolHandler'; import { GitExtensionImpl } from './api/extension'; // import * as path from 'path'; // import * as fs from 'fs'; +import * as os from 'os'; import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; import { TerminalEnvironmentManager } from './terminal'; @@ -39,11 +40,17 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann const askpass = await Askpass.create(outputChannel, context.storagePath); disposables.push(askpass); - const env = askpass.getEnv(); - const terminalEnvironmentManager = new TerminalEnvironmentManager(context, env); + const environment = askpass.getEnv(); + const terminalEnvironmentManager = new TerminalEnvironmentManager(context, environment); disposables.push(terminalEnvironmentManager); - const git = new Git({ gitPath: info.path, version: info.version, env }); + + const git = new Git({ + gitPath: info.path, + userAgent: `git/${info.version} (${(os as any).version?.() ?? os.type()} ${os.release()}; ${os.platform()} ${os.arch()}) azuredatudio`, + version: info.version, + env: environment, + }); const model = new Model(git, askpass, context.globalState, outputChannel); disposables.push(model); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 3fcecc314c..89ff05c2ef 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -12,10 +12,11 @@ import * as path from 'path'; import * as fs from 'fs'; import * as nls from 'vscode-nls'; import { fromGitUri } from './uri'; -import { APIState as State, RemoteSourceProvider, CredentialsProvider, PushErrorHandler } from './api/git'; +import { APIState as State, RemoteSourceProvider, CredentialsProvider, PushErrorHandler, PublishEvent } from './api/git'; import { Askpass } from './askpass'; import { IRemoteSourceProviderRegistry } from './remoteProvider'; import { IPushErrorHandlerRegistry } from './pushError'; +import { ApiRepository } from './api/api1'; const localize = nls.loadMessageBundle(); @@ -69,6 +70,13 @@ export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRe private _onDidChangeState = new EventEmitter(); readonly onDidChangeState = this._onDidChangeState.event; + private _onDidPublish = new EventEmitter(); + readonly onDidPublish = this._onDidPublish.event; + + firePublishEvent(repository: Repository, branch?: string) { + this._onDidPublish.fire({ repository: new ApiRepository(repository), branch: branch }); + } + private _state: State = 'uninitialized'; get state(): State { return this._state; } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 3ec85ce807..af275a6167 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,10 +7,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands } from 'vscode'; import * as nls from 'vscode-nls'; -import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery } from './api/git'; +import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery } from './api/git'; import { AutoFetcher } from './autofetch'; import { debounce, memoize, throttle } from './decorators'; -import { Commit, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; +import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util'; @@ -79,7 +79,7 @@ export class Resource implements SourceControlResourceState { return this.resources[0]; } - get rightUri(): Uri { + get rightUri(): Uri | undefined { return this.resources[1]; } @@ -88,7 +88,7 @@ export class Resource implements SourceControlResourceState { } @memoize - private get resources(): [Uri | undefined, Uri] { + private get resources(): [Uri | undefined, Uri | undefined] { return this._commandResolver.getResources(this); } @@ -613,7 +613,7 @@ class ResourceCommandResolver { } } - getResources(resource: Resource): [Uri | undefined, Uri] { + getResources(resource: Resource): [Uri | undefined, Uri | undefined] { for (const submodule of this.repository.submodules) { if (path.join(this.repository.root, submodule.path) === resource.resourceUri.fsPath) { return [undefined, toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: this.repository.root })]; @@ -641,7 +641,7 @@ class ResourceCommandResolver { return undefined; } - private getRightResource(resource: Resource): Uri { + private getRightResource(resource: Resource): Uri | undefined { switch (resource.type) { case Status.INDEX_MODIFIED: case Status.INDEX_ADDED: @@ -677,7 +677,7 @@ class ResourceCommandResolver { return resource.resourceUri; } - throw new Error('Should never happen'); + return undefined; } private getTitle(resource: Resource): string { @@ -1129,7 +1129,7 @@ export class Repository implements Disposable { return this.run(Operation.HashObject, () => this.repository.hashObject(data)); } - async add(resources: Uri[], opts?: { update?: boolean }): Promise { + async add(resources: Uri[], opts?: { update?: boolean; }): Promise { await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath), opts)); } @@ -1165,6 +1165,12 @@ export class Repository implements Disposable { } delete opts.all; + + if (opts.requireUserConfig === undefined || opts.requireUserConfig === null) { + const config = workspace.getConfiguration('git', Uri.file(this.root)); + opts.requireUserConfig = config.get('requireGitUserConfig'); + } + await this.repository.commit(message, opts); }); } @@ -1260,12 +1266,12 @@ export class Repository implements Disposable { await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name)); } - async checkout(treeish: string, opts?: { detached?: boolean }): Promise { + async checkout(treeish: string, opts?: { detached?: boolean; }): Promise { await this.run(Operation.Checkout, () => this.repository.checkout(treeish, [], opts)); } - async checkoutTracking(treeish: string): Promise { - await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { track: true })); + async checkoutTracking(treeish: string, opts: { detached?: boolean; } = {}): Promise { + await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { ...opts, track: true })); } async findTrackingBranches(upstreamRef: string): Promise { @@ -1297,7 +1303,7 @@ export class Repository implements Disposable { } @throttle - async fetchDefault(options: { silent?: boolean } = {}): Promise { + async fetchDefault(options: { silent?: boolean; } = {}): Promise { await this._fetch({ silent: options.silent }); } @@ -1315,7 +1321,7 @@ export class Repository implements Disposable { await this._fetch({ remote, ref, depth }); } - private async _fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean } = {}): Promise { + private async _fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean; } = {}): Promise { if (!options.prune) { const config = workspace.getConfiguration('git', Uri.file(this.root)); const prune = config.get('pruneOnFetch'); @@ -1363,7 +1369,9 @@ export class Repository implements Disposable { await this.repository.fetch({ all: true }); } - await this.repository.pull(rebase, remote, branch, { unshallow, tags }); + if (await this.checkIfMaybeRebased(this.HEAD?.name)) { + await this.repository.pull(rebase, remote, branch, { unshallow, tags }); + } }); }); } @@ -1432,10 +1440,11 @@ export class Repository implements Disposable { await this.repository.fetch({ all: true, cancellationToken }); } - await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken }); + if (await this.checkIfMaybeRebased(this.HEAD?.name)) { + await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken }); + } }; - if (supportCancellation) { const opts: ProgressOptions = { location: ProgressLocation.Notification, @@ -1463,6 +1472,54 @@ export class Repository implements Disposable { }); } + private async checkIfMaybeRebased(currentBranch?: string) { + const config = workspace.getConfiguration('git'); + const shouldIgnore = config.get('ignoreRebaseWarning') === true; + + if (shouldIgnore) { + return true; + } + + const maybeRebased = await this.run(Operation.Log, async () => { + try { + const result = await this.repository.exec(['log', '--oneline', '--cherry', `${currentBranch ?? ''}...${currentBranch ?? ''}@{upstream}`, '--']); + if (result.exitCode) { + return false; + } + + return /^=/.test(result.stdout); + } catch { + return false; + } + }); + + if (!maybeRebased) { + return true; + } + + const always = { title: localize('always pull', "Always Pull") }; + const pull = { title: localize('pull', "Pull") }; + const cancel = { title: localize('dont pull', "Don't Pull") }; + const result = await window.showWarningMessage( + currentBranch + ? localize('pull branch maybe rebased', "It looks like the current branch \'{0}\' might have been rebased. Are you sure you still want to pull into it?", currentBranch) + : localize('pull maybe rebased', "It looks like the current branch might have been rebased. Are you sure you still want to pull into it?"), + always, pull, cancel + ); + + if (result === pull) { + return true; + } + + if (result === always) { + await config.update('ignoreRebaseWarning', true, true); + + return true; + } + + return false; + } + async show(ref: string, filePath: string): Promise { return await this.run(Operation.Show, async () => { const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); @@ -1490,11 +1547,11 @@ export class Repository implements Disposable { }); } - getObjectDetails(ref: string, filePath: string): Promise<{ mode: string, object: string, size: number }> { + getObjectDetails(ref: string, filePath: string): Promise<{ mode: string, object: string, size: number; }> { return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath)); } - detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> { + detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string; }> { return this.run(Operation.Show, () => this.repository.detectObjectType(object)); } diff --git a/extensions/git/src/test/smoke.test.ts b/extensions/git/src/test/smoke.test.ts index 9fd8722cfc..99580e337d 100644 --- a/extensions/git/src/test/smoke.test.ts +++ b/extensions/git/src/test/smoke.test.ts @@ -124,4 +124,31 @@ suite('git smoke test', function () { assert.equal(repository.state.workingTreeChanges.length, 0); assert.equal(repository.state.indexChanges.length, 0); }); + + test('rename/delete conflict', async function () { + cp.execSync('git branch test', { cwd }); + cp.execSync('git checkout test', { cwd }); + + fs.unlinkSync(file('app.js')); + cp.execSync('git add .', { cwd }); + + await repository.commit('commit on test'); + cp.execSync('git checkout master', { cwd }); + + fs.renameSync(file('app.js'), file('rename.js')); + cp.execSync('git add .', { cwd }); + await repository.commit('commit on master'); + + try { + cp.execSync('git merge test', { cwd }); + } catch (e) { } + + setTimeout(() => { + commands.executeCommand('workbench.scm.focus'); + }, 2e3); + + await new Promise(resolve => { + setTimeout(resolve, 5e3); + }); + }); }); diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index b74a91843a..0fe917632f 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -3,6 +3,7 @@ "displayName": "%displayName%", "description": "%description%", "publisher": "vscode", + "license": "MIT", "version": "0.0.1", "engines": { "vscode": "^1.41.0" @@ -58,10 +59,12 @@ "vscode-nls": "^4.1.2" }, "devDependencies": { - "@types/keytar": "^4.4.2", - "@types/node": "^10.12.21", - "@types/node-fetch": "2.5.7", - "@types/uuid": "8.0.0", - "typescript": "^3.7.5" + "@types/node": "^12.19.9", + "@types/node-fetch": "^2.5.7", + "@types/uuid": "8.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/github-authentication/src/common/keychain.ts b/extensions/github-authentication/src/common/keychain.ts index bf331b1335..959e340140 100644 --- a/extensions/github-authentication/src/common/keychain.ts +++ b/extensions/github-authentication/src/common/keychain.ts @@ -31,9 +31,10 @@ export type Keytar = { const SERVICE_ID = `github.auth`; export class Keychain { + constructor(private context: vscode.ExtensionContext) { } async setToken(token: string): Promise { try { - return await vscode.authentication.setPassword(SERVICE_ID, token); + return await this.context.secrets.store(SERVICE_ID, token); } catch (e) { // Ignore Logger.error(`Setting token failed: ${e}`); @@ -47,7 +48,7 @@ export class Keychain { async getToken(): Promise { try { - return await vscode.authentication.getPassword(SERVICE_ID); + return await this.context.secrets.get(SERVICE_ID); } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); @@ -57,7 +58,7 @@ export class Keychain { async deleteToken(): Promise { try { - return await vscode.authentication.deletePassword(SERVICE_ID); + return await this.context.secrets.delete(SERVICE_ID); } catch (e) { // Ignore Logger.error(`Deleting token failed: ${e}`); @@ -85,5 +86,3 @@ export class Keychain { } } } - -export const keychain = new Keychain(); diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 3de3bd5d54..018cf24f73 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(); + const loginService = new GitHubAuthenticationProvider(context); await loginService.initialize(context); @@ -22,10 +22,7 @@ export async function activate(context: vscode.ExtensionContext) { return loginService.manuallyProvideToken(); })); - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ - id: 'github', - label: 'GitHub', - supportsMultipleAccounts: false, + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('github', 'GitHub', { onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), login: async (scopeList: string[]) => { @@ -79,7 +76,7 @@ export async function activate(context: vscode.ExtensionContext) { throw e; } } - })); + }, { supportsMultipleAccounts: false })); return; } diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index e49d8271b0..5aa414cbe9 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { v4 as uuid } from 'uuid'; -import { keychain } from './common/keychain'; +import { Keychain } from './common/keychain'; import { GitHubServer, NETWORK_ERROR } from './githubServer'; import Logger from './common/logger'; @@ -26,6 +26,12 @@ export class GitHubAuthenticationProvider { private _sessions: vscode.AuthenticationSession[] = []; private _githubServer = new GitHubServer(); + private _keychain: Keychain; + + constructor(context: vscode.ExtensionContext) { + this._keychain = new Keychain(context); + } + public async initialize(context: vscode.ExtensionContext): Promise { try { this._sessions = await this.readSessions(); @@ -34,7 +40,7 @@ export class GitHubAuthenticationProvider { // Ignore, network request failed } - context.subscriptions.push(vscode.authentication.onDidChangePassword(() => this.checkForUpdates())); + context.subscriptions.push(context.secrets.onDidChange(() => this.checkForUpdates())); } private async verifySessions(): Promise { @@ -101,7 +107,7 @@ export class GitHubAuthenticationProvider { } private async readSessions(): Promise { - const storedSessions = await keychain.getToken() || await keychain.tryMigrate(); + const storedSessions = await this._keychain.getToken() || await this._keychain.tryMigrate(); if (storedSessions) { try { const sessionData: SessionData[] = JSON.parse(storedSessions); @@ -132,7 +138,7 @@ export class GitHubAuthenticationProvider { } Logger.error(`Error reading sessions: ${e}`); - await keychain.deleteToken(); + await this._keychain.deleteToken(); } } @@ -140,7 +146,7 @@ export class GitHubAuthenticationProvider { } private async storeSessions(): Promise { - await keychain.setToken(JSON.stringify(this._sessions)); + await this._keychain.setToken(JSON.stringify(this._sessions)); } get sessions(): vscode.AuthenticationSession[] { diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 08604b972e..1da6beed52 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -2,14 +2,7 @@ # yarn lockfile v1 -"@types/keytar@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.2.tgz#49ef917d6cbb4f19241c0ab50cd35097b5729b32" - integrity sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw== - dependencies: - keytar "*" - -"@types/node-fetch@2.5.7": +"@types/node-fetch@^2.5.7": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== @@ -22,26 +15,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@^10.12.21": - version "10.17.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944" - integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== "@types/uuid@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -51,56 +34,11 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -bl@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" - integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -buffer@^5.5.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" - integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -108,43 +46,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== - dependencies: - mimic-response "^2.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" @@ -157,18 +63,6 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -178,75 +72,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -keytar@*: - version "6.0.1" - resolved "https://registry.yarnpkg.com/keytar/-/keytar-6.0.1.tgz#996961abdebf300b2d34bb2eab6e42a8096b1ed8" - integrity sha512-1Ihpf2tdM3sLwGMkYHXYhVC/hx5BDR7CWFL4IrBA3IDZo0xHhS2nM+tU9Y+u/U7okNfbVkwmKsieLkcWRMh93g== - dependencies: - node-addon-api "^3.0.0" - prebuild-install "5.3.4" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -259,275 +84,16 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== - -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - -node-abi@^2.7.0: - version "2.19.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.1.tgz#6aa32561d0a5e2fdb6810d8c25641b657a8cea85" - integrity sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A== - dependencies: - semver "^5.4.1" - -node-addon-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.0.tgz#812446a1001a54f71663bed188314bba07e09247" - integrity sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg== - 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== -noop-logger@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" - integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= - -npmlog@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -prebuild-install@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.4.tgz#6982d10084269d364c1856550b7d090ea31fa293" - integrity sha512-AkKN+pf4fSEihjapLEEj8n85YIw/tN6BQqkhzbDc0RvEZGdkpJBGMUYx66AAMcPG2KzmPQS7Cm16an4HVBRRMA== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp "^0.5.1" - napi-build-utils "^1.0.1" - node-abi "^2.7.0" - noop-logger "^0.1.1" - npmlog "^4.0.1" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^3.0.3" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - which-pm-runs "^1.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -safe-buffer@^5.0.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -semver@^5.3.0, semver@^5.4.1: +semver@^5.3.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -signal-exit@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -tar-fs@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" - integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.0.0" - -tar-stream@^2.0.0: - version "2.1.4" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" - integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -typescript@^3.7.5: - version "3.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" - integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - uuid@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" @@ -545,23 +111,6 @@ vscode-nls@^4.1.2: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - zone.js@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" diff --git a/extensions/github/package.json b/extensions/github/package.json index 464453d399..f6e7cd6bdb 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -3,6 +3,7 @@ "displayName": "%displayName%", "description": "%description%", "publisher": "vscode", + "license": "MIT", "version": "0.0.1", "engines": { "vscode": "^1.41.0" @@ -62,6 +63,10 @@ "vscode-nls": "^4.1.2" }, "devDependencies": { - "@types/node": "^10.12.21" + "@types/node": "^12.19.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index a9e0ba38b6..5e790ac290 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -104,10 +104,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@^10.12.21": - version "10.17.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.14.tgz#b6c60ebf2fb5e4229fdd751ff9ddfae0f5f31541" - integrity sha512-G0UmX5uKEmW+ZAhmZ6PLTQ5eu/VPaT+d/tdLd5IFsKRPcbe6lPxocBtcYBFSaLaCW8O60AX90e91Nsp8lVHCNw== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== before-after-hook@^2.1.0: version "2.1.0" diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index 064ada9485..5f49df1d60 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -34,7 +34,7 @@ "priority": "builtin", "selector": [ { - "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp}" + "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp,avif}" } ] } @@ -77,5 +77,9 @@ "dependencies": { "vscode-extension-telemetry": "0.1.1", "vscode-nls": "^4.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/import/src/test/utils.test.ts b/extensions/import/src/test/utils.test.ts index 19e2219cce..f603240c4c 100644 --- a/extensions/import/src/test/utils.test.ts +++ b/extensions/import/src/test/utils.test.ts @@ -152,6 +152,7 @@ export class TestExtensionContext implements vscode.ExtensionContext { storagePath: string; globalStoragePath: string; logPath: string; + secrets: vscode.SecretStorage; } export class TestImportDataModel implements ImportDataModel { diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 5f1eb6c2e5..5ecfe4b760 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -21,15 +21,15 @@ import { hash } from './utils/hash'; import { RequestService, joinPath } from './requests'; namespace VSCodeContentRequest { - export const type: RequestType = new RequestType('vscode/content'); + export const type: RequestType = new RequestType('vscode/content'); } namespace SchemaContentChangeNotification { - export const type: NotificationType = new NotificationType('json/schemaContent'); + export const type: NotificationType = new NotificationType('json/schemaContent'); } namespace ForceValidateRequest { - export const type: RequestType = new RequestType('json/validate'); + export const type: RequestType = new RequestType('json/validate'); } export interface ISchemaAssociations { @@ -42,11 +42,11 @@ export interface ISchemaAssociation { } namespace SchemaAssociationNotification { - export const type: NotificationType = new NotificationType('json/schemaAssociations'); + export const type: NotificationType = new NotificationType('json/schemaAssociations'); } namespace ResultLimitReachedNotification { - export const type: NotificationType = new NotificationType('json/resultLimitReached'); + export const type: NotificationType = new NotificationType('json/resultLimitReached'); } interface Settings { @@ -325,12 +325,17 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua } else if (formatEnabled && !rangeFormatting) { rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { + const filesConfig = workspace.getConfiguration('files', document); + const fileFormattingOptions = { + trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), + trimFinalNewlines: filesConfig.get('trimFinalNewlines'), + insertFinalNewline: filesConfig.get('insertFinalNewline'), + }; const params: DocumentRangeFormattingParams = { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), range: client.code2ProtocolConverter.asRange(range), - options: client.code2ProtocolConverter.asFormattingOptions(options) + options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions) }; - params.options.insertFinalNewline = workspace.getConfiguration('files', document).get('insertFinalNewline'); return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( client.protocol2CodeConverter.asTextEdits, diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index a8e3231a93..e6c318f5e4 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -1,138 +1,144 @@ { - "name": "json-language-features", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", - "engines": { - "vscode": "0.10.x" - }, - "icon": "icons/json.png", - "activationEvents": [ - "onLanguage:json", - "onLanguage:jsonc" - ], - "main": "./client/out/node/jsonClientMain", - "browser": "./client/dist/browser/jsonClientMain", - "enableProposedApi": true, - "scripts": { - "compile": "gulp compile-extension:json-language-features-client compile-extension:json-language-features-server", - "watch": "gulp watch-extension:json-language-features-client watch-extension:json-language-features-server", + "name": "json-language-features", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "engines": { + "vscode": "0.10.x" + }, + "icon": "icons/json.png", + "activationEvents": [ + "onLanguage:json", + "onLanguage:jsonc" + ], + "main": "./client/out/node/jsonClientMain", + "browser": "./client/dist/browser/jsonClientMain", + "enableProposedApi": true, + "scripts": { + "compile": "gulp compile-extension:json-language-features-client compile-extension:json-language-features-server", + "watch": "gulp watch-extension:json-language-features-client watch-extension:json-language-features-server", "postinstall": "cd server && yarn install", - "install-client-next": "yarn add vscode-languageclient@next" - }, - "categories": [ - "Programming Languages" - ], - "contributes": { - "configuration": { - "id": "json", - "order": 20, + "install-client-next": "yarn add vscode-languageclient@next" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "configuration": { + "id": "json", + "order": 20, + "type": "object", + "title": "JSON", + "properties": { + "json.schemas": { + "type": "array", + "scope": "resource", + "description": "%json.schemas.desc%", + "items": { "type": "object", - "title": "JSON", + "default": { + "fileMatch": [ + "/myfile" + ], + "url": "schemaURL" + }, "properties": { - "json.schemas": { - "type": "array", - "scope": "resource", - "description": "%json.schemas.desc%", - "items": { - "type": "object", - "default": { - "fileMatch": [ - "/myfile" - ], - "url": "schemaURL" - }, - "properties": { - "url": { - "type": "string", - "default": "/user.schema.json", - "description": "%json.schemas.url.desc%" - }, - "fileMatch": { - "type": "array", - "items": { - "type": "string", - "default": "MyFile.json", - "description": "%json.schemas.fileMatch.item.desc%" - }, - "minItems": 1, - "description": "%json.schemas.fileMatch.desc%" - }, - "schema": { - "$ref": "http://json-schema.org/draft-07/schema#", - "description": "%json.schemas.schema.desc%" - } - } - } + "url": { + "type": "string", + "default": "/user.schema.json", + "description": "%json.schemas.url.desc%" + }, + "fileMatch": { + "type": "array", + "items": { + "type": "string", + "default": "MyFile.json", + "description": "%json.schemas.fileMatch.item.desc%" }, - "json.format.enable": { - "type": "boolean", - "scope": "window", - "default": true, - "description": "%json.format.enable.desc%" - }, - "json.trace.server": { - "type": "string", - "scope": "window", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "off", - "description": "%json.tracing.desc%" - }, - "json.colorDecorators.enable": { - "type": "boolean", - "scope": "window", - "default": true, - "description": "%json.colorDecorators.enable.desc%", - "deprecationMessage": "%json.colorDecorators.enable.deprecationMessage%" - }, - "json.maxItemsComputed": { - "type": "number", - "default": 5000, - "description": "%json.maxItemsComputed.desc%" - }, - "json.schemaDownload.enable": { - "type": "boolean", - "default": true, - "description": "%json.enableSchemaDownload.desc%", - "tags": ["usesOnlineServices"] + "minItems": 1, + "description": "%json.schemas.fileMatch.desc%" + }, + "schema": { + "$ref": "http://json-schema.org/draft-07/schema#", + "description": "%json.schemas.schema.desc%" } } + } }, - "configurationDefaults": { - "[json]": { - "editor.quickSuggestions": { - "strings": true - }, - "editor.suggest.insertMode": "replace" - }, - "[jsonc]": { - "editor.quickSuggestions": { - "strings": true - }, - "editor.suggest.insertMode": "replace" - } + "json.format.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%json.format.enable.desc%" }, - "jsonValidation": [ - { - "fileMatch": "*.schema.json", - "url": "http://json-schema.org/draft-07/schema#" - } - ] + "json.trace.server": { + "type": "string", + "scope": "window", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "%json.tracing.desc%" + }, + "json.colorDecorators.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%json.colorDecorators.enable.desc%", + "deprecationMessage": "%json.colorDecorators.enable.deprecationMessage%" + }, + "json.maxItemsComputed": { + "type": "number", + "default": 5000, + "description": "%json.maxItemsComputed.desc%" + }, + "json.schemaDownload.enable": { + "type": "boolean", + "default": true, + "description": "%json.enableSchemaDownload.desc%", + "tags": [ + "usesOnlineServices" + ] + } + } }, - "dependencies": { - "request-light": "^0.4.0", - "vscode-extension-telemetry": "0.1.1", - "vscode-languageclient": "7.0.0-next.5.1", - "vscode-nls": "^5.0.0" + "configurationDefaults": { + "[json]": { + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace" + }, + "[jsonc]": { + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace" + } }, - "devDependencies": { - "@types/node": "^12.11.7" - } + "jsonValidation": [ + { + "fileMatch": "*.schema.json", + "url": "http://json-schema.org/draft-07/schema#" + } + ] + }, + "dependencies": { + "request-light": "^0.4.0", + "vscode-extension-telemetry": "0.1.1", + "vscode-languageclient": "^7.0.0", + "vscode-nls": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^12.19.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 86df32285a..6d1e3143b9 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,13 +14,13 @@ "dependencies": { "jsonc-parser": "^3.0.0", "request-light": "^0.4.0", - "vscode-json-languageservice": "^3.11.0", - "vscode-languageserver": "7.0.0-next.3", - "vscode-uri": "^2.1.2" + "vscode-json-languageservice": "^4.0.2", + "vscode-languageserver": "^7.0.0", + "vscode-uri": "^3.0.2" }, "devDependencies": { - "@types/mocha": "2.2.33", - "@types/node": "^12.11.7" + "@types/mocha": "^8.2.0", + "@types/node": "^12.19.9" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index 106fe00d1f..e7a5e11e7f 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -17,23 +17,23 @@ import { RequestService, basename, resolvePath } from './requests'; type ISchemaAssociations = Record; namespace SchemaAssociationNotification { - export const type: NotificationType = new NotificationType('json/schemaAssociations'); + export const type: NotificationType = new NotificationType('json/schemaAssociations'); } namespace VSCodeContentRequest { - export const type: RequestType = new RequestType('vscode/content'); + export const type: RequestType = new RequestType('vscode/content'); } namespace SchemaContentChangeNotification { - export const type: NotificationType = new NotificationType('json/schemaContent'); + export const type: NotificationType = new NotificationType('json/schemaContent'); } namespace ResultLimitReachedNotification { - export const type: NotificationType = new NotificationType('json/resultLimitReached'); + export const type: NotificationType = new NotificationType('json/resultLimitReached'); } namespace ForceValidateRequest { - export const type: RequestType = new RequestType('json/validate'); + export const type: RequestType = new RequestType('json/validate'); } diff --git a/extensions/json-language-features/server/src/utils/runner.ts b/extensions/json-language-features/server/src/utils/runner.ts index b2a16c7ac6..5a683a6501 100644 --- a/extensions/json-language-features/server/src/utils/runner.ts +++ b/extensions/json-language-features/server/src/utils/runner.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver'; +import { CancellationToken, ResponseError, LSPErrorCodes } from 'vscode-languageserver'; export function formatError(message: string, err: any): string { if (err instanceof Error) { @@ -64,5 +64,5 @@ export function runSafe(func: () => T, errorVal: T, errorMessage: string, function cancelValue() { console.log('cancelled'); - return new ResponseError(ErrorCodes.RequestCancelled, 'Request cancelled'); + return new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled'); } diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index ec82f87b58..a49955024a 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@types/mocha@2.2.33": - version "2.2.33" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.33.tgz#d79a0061ec270379f4d9e225f4096fb436669def" - integrity sha1-15oAYewnA3n02eIl9AlvtDZmne8= +"@types/mocha@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" + integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== agent-base@4: version "4.1.2" @@ -80,46 +80,46 @@ request-light@^0.4.0: https-proxy-agent "^2.2.4" vscode-nls "^4.1.2" -vscode-json-languageservice@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.11.0.tgz#ad574b36c4346bd7830f1d34b5a5213d3af8d232" - integrity sha512-QxI+qV97uD7HHOCjh3MrM1TfbdwmTXrMckri5Tus1/FQiG3baDZb2C9Y0y8QThs7PwHYBIQXcAc59ZveCRZKPA== +vscode-json-languageservice@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.0.2.tgz#8f91dc3a33dac180063067f8277f4facdc0795b6" + integrity sha512-d8Ahw990Cq/G60CzN26rehXcbhbMgMGMmXeN6C/V/RYZUhfs16EELRK+EL7b/3Y8ZGshtKqboePSeDVa94qqFg== dependencies: jsonc-parser "^3.0.0" vscode-languageserver-textdocument "^1.0.1" - vscode-languageserver-types "3.16.0-next.2" + vscode-languageserver-types "^3.16.0" vscode-nls "^5.0.0" - vscode-uri "^2.1.2" + vscode-uri "^3.0.2" -vscode-jsonrpc@6.0.0-next.2: - version "6.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f" - integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw== +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== -vscode-languageserver-protocol@3.16.0-next.4: - version "3.16.0-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f" - integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ== +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== dependencies: - vscode-jsonrpc "6.0.0-next.2" - vscode-languageserver-types "3.16.0-next.2" + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" vscode-languageserver-textdocument@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== -vscode-languageserver-types@3.16.0-next.2: - version "3.16.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083" - integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q== +vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== -vscode-languageserver@7.0.0-next.3: - version "7.0.0-next.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0-next.3.tgz#3833bd09259a4a085baeba90783f1e4d06d81095" - integrity sha512-qSt8eb546iFuoFIN+9MPl4Avru6Iz2/JP0UmS/3djf40ICa31Np/yJ7anX2j0Az5rCzb0fak8oeKwDioGeVOYg== +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== dependencies: - vscode-languageserver-protocol "3.16.0-next.4" + vscode-languageserver-protocol "3.16.0" vscode-nls@^4.1.2: version "4.1.2" @@ -131,7 +131,7 @@ vscode-nls@^5.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== -vscode-uri@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c" - integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A== +vscode-uri@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0" + integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA== diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index a1e87bdbce..8946235df5 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== agent-base@4: version "4.2.1" @@ -30,6 +30,24 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -84,6 +102,20 @@ https-proxy-agent@^2.2.4: agent-base "^4.3.0" debug "^3.1.0" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -108,10 +140,12 @@ semver@^5.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" vscode-extension-telemetry@0.1.1: version "0.1.1" @@ -120,31 +154,32 @@ vscode-extension-telemetry@0.1.1: dependencies: applicationinsights "1.0.8" -vscode-jsonrpc@6.0.0-next.2: - version "6.0.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f" - integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw== +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== -vscode-languageclient@7.0.0-next.5.1: - version "7.0.0-next.5.1" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0-next.5.1.tgz#ed93f14e4c2cdccedf15002c7bf8ef9cb638f36c" - integrity sha512-OONvbk3IFpubwF8/Y5uPQaq5J5CEskpeET3SfK4iGlv5OUK+44JawH/SEW5wXuEPpfdMLEMZLuGLU5v5d7N7PQ== +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== dependencies: - semver "^6.3.0" - vscode-languageserver-protocol "3.16.0-next.4" + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" -vscode-languageserver-protocol@3.16.0-next.4: - version "3.16.0-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f" - integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ== +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== dependencies: - vscode-jsonrpc "6.0.0-next.2" - vscode-languageserver-types "3.16.0-next.2" + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" -vscode-languageserver-types@3.16.0-next.2: - version "3.16.0-next.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083" - integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q== +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== vscode-nls@^4.1.2: version "4.1.2" @@ -156,6 +191,11 @@ vscode-nls@^5.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + zone.js@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" diff --git a/extensions/json/build/update-grammars.js b/extensions/json/build/update-grammars.js index 6e650f1f43..37a8fe5810 100644 --- a/extensions/json/build/update-grammars.js +++ b/extensions/json/build/update-grammars.js @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -var updateGrammar = require('../../../build/npm/update-grammar'); +var updateGrammar = require('vscode-grammar-updater'); function adaptJSON(grammar, replacementScope) { grammar.name = 'JSON with comments'; grammar.scopeName = `source${replacementScope}`; - var fixScopeNames = function(rule) { + var fixScopeNames = function (rule) { if (typeof rule.name === 'string') { rule.name = rule.name.replace(/\.json/g, replacementScope); } diff --git a/extensions/json/package.json b/extensions/json/package.json index 1b7331b207..a4ccc12cf2 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -74,5 +74,9 @@ "path": "./syntaxes/JSONC.tmLanguage.json" } ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/json/package.nls.json b/extensions/json/package.nls.json index 6307b6dcab..ee3b328698 100644 --- a/extensions/json/package.nls.json +++ b/extensions/json/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "JSON Language Basics", "description": "Provides syntax highlighting & bracket matching in JSON files." -} \ No newline at end of file +} diff --git a/extensions/json/yarn.lock b/extensions/json/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/json/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/machine-learning/src/test/mainController.test.ts b/extensions/machine-learning/src/test/mainController.test.ts index b5dea3313c..42dd32f53e 100644 --- a/extensions/machine-learning/src/test/mainController.test.ts +++ b/extensions/machine-learning/src/test/mainController.test.ts @@ -96,7 +96,8 @@ function createContext(): TestContext { extensionMode: undefined as any, globalStorageUri: vscode.Uri.parse('test://'), logUri: vscode.Uri.parse('test://'), - storageUri: undefined + storageUri: undefined, + secrets: undefined as any }, outputChannel: { name: '', diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 5606dcc7cd..92288d403b 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,12 +33,12 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "11cf764606cb2cde54badb5d0e5a0758a8871c4b" + "commitHash": "7019b191c3ee38b6c345f3a2a843f223eb92ca1e" } }, "license": "MIT", - "version": "0.0.0" + "version": "1.0.0" } ], "version": 1 -} +} \ No newline at end of file diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index bbc5e342db..8ea3bff2e3 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -1,95 +1,99 @@ { - "name": "markdown", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { - "vscode": "^1.20.0" - }, - "contributes": { - "languages": [ - { - "id": "markdown", - "aliases": [ - "Markdown", - "markdown" - ], - "extensions": [ - ".md", - ".mkd", - ".mdwn", - ".mdown", - ".markdown", - ".markdn", - ".mdtxt", - ".mdtext", - ".workbook" - ], - "configuration": "./language-configuration.json" - } - ], - "grammars": [ - { - "language": "markdown", - "scopeName": "text.html.markdown", - "path": "./syntaxes/markdown.tmLanguage.json", - "embeddedLanguages": { - "meta.embedded.block.html": "html", - "source.js": "javascript", - "source.css": "css", - "meta.embedded.block.frontmatter": "yaml", - "meta.embedded.block.css": "css", - "meta.embedded.block.ini": "ini", - "meta.embedded.block.java": "java", - "meta.embedded.block.lua": "lua", - "meta.embedded.block.makefile": "makefile", - "meta.embedded.block.perl": "perl", - "meta.embedded.block.r": "r", - "meta.embedded.block.ruby": "ruby", - "meta.embedded.block.php": "php", - "meta.embedded.block.sql": "sql", - "meta.embedded.block.vs_net": "vs_net", - "meta.embedded.block.xml": "xml", - "meta.embedded.block.xsl": "xsl", - "meta.embedded.block.yaml": "yaml", - "meta.embedded.block.dosbatch": "dosbatch", - "meta.embedded.block.clojure": "clojure", - "meta.embedded.block.coffee": "coffee", - "meta.embedded.block.c": "c", - "meta.embedded.block.cpp": "cpp", - "meta.embedded.block.diff": "diff", - "meta.embedded.block.dockerfile": "dockerfile", - "meta.embedded.block.go": "go", - "meta.embedded.block.groovy": "groovy", - "meta.embedded.block.pug": "jade", - "meta.embedded.block.javascript": "javascript", - "meta.embedded.block.json": "json", - "meta.embedded.block.less": "less", - "meta.embedded.block.objc": "objc", - "meta.embedded.block.scss": "scss", - "meta.embedded.block.perl6": "perl6", - "meta.embedded.block.powershell": "powershell", - "meta.embedded.block.python": "python", - "meta.embedded.block.rust": "rust", - "meta.embedded.block.scala": "scala", - "meta.embedded.block.shellscript": "shellscript", - "meta.embedded.block.typescript": "typescript", - "meta.embedded.block.typescriptreact": "typescriptreact", - "meta.embedded.block.csharp": "csharp", - "meta.embedded.block.fsharp": "fsharp" - } - } - ], - "snippets": [ - { - "language": "markdown", - "path": "./snippets/markdown.code-snippets" - } - ] - }, - "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js microsoft/vscode-markdown-tm-grammar syntaxes/markdown.tmLanguage ./syntaxes/markdown.tmLanguage.json" - } + "name": "markdown", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.20.0" + }, + "contributes": { + "languages": [ + { + "id": "markdown", + "aliases": [ + "Markdown", + "markdown" + ], + "extensions": [ + ".md", + ".mkd", + ".mdwn", + ".mdown", + ".markdown", + ".markdn", + ".mdtxt", + ".mdtext", + ".workbook" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "markdown", + "scopeName": "text.html.markdown", + "path": "./syntaxes/markdown.tmLanguage.json", + "embeddedLanguages": { + "meta.embedded.block.html": "html", + "source.js": "javascript", + "source.css": "css", + "meta.embedded.block.frontmatter": "yaml", + "meta.embedded.block.css": "css", + "meta.embedded.block.ini": "ini", + "meta.embedded.block.java": "java", + "meta.embedded.block.lua": "lua", + "meta.embedded.block.makefile": "makefile", + "meta.embedded.block.perl": "perl", + "meta.embedded.block.r": "r", + "meta.embedded.block.ruby": "ruby", + "meta.embedded.block.php": "php", + "meta.embedded.block.sql": "sql", + "meta.embedded.block.vs_net": "vs_net", + "meta.embedded.block.xml": "xml", + "meta.embedded.block.xsl": "xsl", + "meta.embedded.block.yaml": "yaml", + "meta.embedded.block.dosbatch": "dosbatch", + "meta.embedded.block.clojure": "clojure", + "meta.embedded.block.coffee": "coffee", + "meta.embedded.block.c": "c", + "meta.embedded.block.cpp": "cpp", + "meta.embedded.block.diff": "diff", + "meta.embedded.block.dockerfile": "dockerfile", + "meta.embedded.block.go": "go", + "meta.embedded.block.groovy": "groovy", + "meta.embedded.block.pug": "jade", + "meta.embedded.block.javascript": "javascript", + "meta.embedded.block.json": "json", + "meta.embedded.block.less": "less", + "meta.embedded.block.objc": "objc", + "meta.embedded.block.scss": "scss", + "meta.embedded.block.perl6": "perl6", + "meta.embedded.block.powershell": "powershell", + "meta.embedded.block.python": "python", + "meta.embedded.block.rust": "rust", + "meta.embedded.block.scala": "scala", + "meta.embedded.block.shellscript": "shellscript", + "meta.embedded.block.typescript": "typescript", + "meta.embedded.block.typescriptreact": "typescriptreact", + "meta.embedded.block.csharp": "csharp", + "meta.embedded.block.fsharp": "fsharp" + } + } + ], + "snippets": [ + { + "language": "markdown", + "path": "./snippets/markdown.code-snippets" + } + ] + }, + "scripts": { + "update-grammar": "node ../node_modules/.bin/vscode-grammar-updater microsoft/vscode-markdown-tm-grammar syntaxes/markdown.tmLanguage ./syntaxes/markdown.tmLanguage.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index 13ee6f3604..a61af0d0c0 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/f051a36bd9713dd722cbe1bdde9c8240d12f00b4", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/7019b191c3ee38b6c345f3a2a843f223eb92ca1e", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -2574,7 +2574,7 @@ "name": "punctuation.definition.link.markdown" } }, - "match": "(<)((?:mailto:)?[-.\\w]+@[-a-z0-9]+(\\.[-a-z0-9]+)*\\.[a-z]+)(>)", + "match": "(<)((?:mailto:)?[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)(>)", "name": "meta.link.email.lt-gt.markdown" }, "link-inet": { diff --git a/extensions/markdown-basics/yarn.lock b/extensions/markdown-basics/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/markdown-basics/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/markdown-language-features/media/index.js b/extensions/markdown-language-features/media/index.js index 0433f33c89..3215cd6a04 100644 --- a/extensions/markdown-language-features/media/index.js +++ b/extensions/markdown-language-features/media/index.js @@ -1 +1 @@ -!function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});let o=void 0;function i(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error(`Could not load data for ${e}`)}t.getData=i,t.getSettings=function(){if(o)return o;if(o=i("data-settings"))return o;throw new Error("Could not load settings")}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(0),i="code-line";function r(e){return t=0,n=o.getSettings().lineCount-1,i=e,Math.min(n,Math.max(t,i));var t,n,i}const c=(()=>{let e;return()=>{if(!e){e=[{element:document.body,line:0}];for(const t of document.getElementsByClassName(i)){const n=+t.getAttribute("data-line");isNaN(n)||("CODE"===t.tagName&&t.parentElement&&"PRE"===t.parentElement.tagName?e.push({element:t.parentElement,line:n}):e.push({element:t,line:n}))}}return e}})();function s(e){const t=Math.floor(e),n=c();let o=n[0]||null;for(const e of n){if(e.line===t)return{previous:e,next:void 0};if(e.line>t)return{previous:o,next:e};o=e}return{previous:o}}function a(e){const t=c(),n=e-window.scrollY;let o=-1,i=t.length-1;for(;o+1=n?i=e:o=e}const r=t[i],s=u(r);if(i>=1&&s.top>n){return{previous:t[o],next:r}}return i>1&&in?{previous:r,next:t[i+1]}:{previous:r}}function u({element:e}){const t=e.getBoundingClientRect(),n=e.querySelector(`.${i}`);if(n){const e=n.getBoundingClientRect(),o=Math.max(1,e.top-t.top);return{top:t.top,height:o}}return t}t.getElementsForSourceLine=s,t.getLineElementsAtPageOffset=a,t.scrollToRevealSourceLine=function(e){if(!o.getSettings().scrollPreviewWithEditor)return;if(e<=0)return void window.scroll(window.scrollX,0);const{previous:t,next:n}=s(e);if(!t)return;let i=0;const r=u(t),c=r.top;if(n&&n.line!==t.line){i=c+(e-t.line)/(n.line-t.line)*(n.element.getBoundingClientRect().top-c)}else{const t=e-Math.floor(e);i=c+r.height*t}window.scroll(window.scrollX,Math.max(1,window.scrollY+i))},t.getEditorLineNumberForPageOffset=function(e){const{previous:t,next:n}=a(e);if(t){const o=u(t),i=e-window.scrollY-o.top;if(n){const e=i/(u(n).top-o.top);return r(t.line+e*(n.line-t.line))}{const e=i/o.height;return r(t.line+e)}}return null},t.getLineElementForFragment=function(e){return c().find(t=>t.element.id===e)}},function(e,t,n){"use strict";(function(e){Object.defineProperty(t,"__esModule",{value:!0});const o=n(7),i=n(8),r=n(9),c=n(2),s=n(0),a=n(10);let u=!0;const l=new o.ActiveLineMarker,f=s.getSettings(),d=acquireVsCodeApi(),m=d.getState(),p={..."object"==typeof m?m:{},...s.getData("data-state")};d.setState(p);const g=r.createPosterForVsCode(d);window.cspAlerter.setPoster(g),window.styleLoadingMonitor.setPoster(g),window.onload=()=>{v()},i.onceDocumentLoaded(()=>{const t=p.scrollProgress;"number"!=typeof t||f.fragment?f.scrollPreviewWithEditor&&e(()=>{if(f.fragment){p.fragment=void 0,d.setState(p);const e=c.getLineElementForFragment(f.fragment);e&&(u=!0,c.scrollToRevealSourceLine(e.line))}else isNaN(f.line)||(u=!0,c.scrollToRevealSourceLine(f.line))}):e(()=>{u=!0,window.scrollTo(0,t*document.body.clientHeight)})});const h=(()=>{const e=a(e=>{u=!0,c.scrollToRevealSourceLine(e)},50);return t=>{isNaN(t)||(p.line=t,e(t))}})();let v=a(()=>{const e=[];let t=document.getElementsByTagName("img");if(t){let n;for(n=0;n{u=!0,w(),v()},!0),window.addEventListener("message",e=>{if(e.data.source===f.source)switch(e.data.type){case"onDidChangeTextEditorSelection":l.onDidChangeTextEditorSelection(e.data.line);break;case"updateView":h(e.data.line)}},!1),document.addEventListener("dblclick",e=>{if(!f.doubleClickToSwitchToEditor)return;for(let t=e.target;t;t=t.parentNode)if("A"===t.tagName)return;const t=e.pageY,n=c.getEditorLineNumberForPageOffset(t);"number"!=typeof n||isNaN(n)||g.postMessage("didClick",{line:Math.floor(n)})});const y=["http:","https:","mailto:","vscode:","vscode-insiders:"];function w(){p.scrollProgress=window.scrollY/document.body.clientHeight,d.setState(p)}document.addEventListener("click",e=>{if(!e)return;let t=e.target;for(;t;){if(t.tagName&&"A"===t.tagName&&t.href){if(t.getAttribute("href").startsWith("#"))return;let n=t.getAttribute("data-href");if(!n){if(y.some(e=>t.href.startsWith(e)))return;n=t.getAttribute("href")}return/^[a-z\-]+:/i.test(n)?void 0:(g.postMessage("openLink",{href:n}),e.preventDefault(),void e.stopPropagation())}t=t.parentNode}},!0),window.addEventListener("scroll",a(()=>{if(w(),u)u=!1;else{const e=c.getEditorLineNumberForPageOffset(window.scrollY);"number"!=typeof e||isNaN(e)||g.postMessage("revealLine",{line:e})}},50))}).call(this,n(4).setImmediate)},function(e,t,n){(function(e){var o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,window,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,window,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(window,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout((function(){e._onTimeout&&e._onTimeout()}),t))},n(5),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(1))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var o,i,r,c,s,a=1,u={},l=!1,f=e.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(e);d=d&&d.setTimeout?d:e,"[object process]"==={}.toString.call(e.process)?o=function(e){t.nextTick((function(){p(e)}))}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((r=new MessageChannel).port1.onmessage=function(e){p(e.data)},o=function(e){r.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(i=f.documentElement,o=function(e){var t=f.createElement("script");t.onreadystatechange=function(){p(e),t.onreadystatechange=null,i.removeChild(t),t=null},i.appendChild(t)}):o=function(e){setTimeout(p,0,e)}:(c="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(c)&&p(+t.data.slice(c.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),o=function(t){e.postMessage(c+t,"*")}),d.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n1)for(var n=1;nnew class{postMessage(t,n){e.postMessage({type:t,source:o.getSettings().source,body:n})}}},function(e,t,n){(function(t){var n="Expected a function",o=NaN,i="[object Symbol]",r=/^\s+|\s+$/g,c=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,a=/^0o[0-7]+$/i,u=parseInt,l="object"==typeof t&&t&&t.Object===Object&&t,f="object"==typeof self&&self&&self.Object===Object&&self,d=l||f||Function("return this")(),m=Object.prototype.toString,p=Math.max,g=Math.min,h=function(){return d.Date.now()};function v(e,t,o){var i,r,c,s,a,u,l=0,f=!1,d=!1,m=!0;if("function"!=typeof e)throw new TypeError(n);function v(t){var n=i,o=r;return i=r=void 0,l=t,s=e.apply(o,n)}function b(e){var n=e-u;return void 0===u||n>=t||n<0||d&&e-l>=c}function T(){var e=h();if(b(e))return E(e);a=setTimeout(T,function(e){var n=t-(e-u);return d?g(n,c-(e-l)):n}(e))}function E(e){return a=void 0,m&&i?v(e):(i=r=void 0,s)}function _(){var e=h(),n=b(e);if(i=arguments,r=this,u=e,n){if(void 0===a)return function(e){return l=e,a=setTimeout(T,t),f?v(e):s}(u);if(d)return a=setTimeout(T,t),v(u)}return void 0===a&&(a=setTimeout(T,t)),s}return t=w(t)||0,y(o)&&(f=!!o.leading,c=(d="maxWait"in o)?p(w(o.maxWait)||0,t):c,m="trailing"in o?!!o.trailing:m),_.cancel=function(){void 0!==a&&clearTimeout(a),l=0,i=u=r=a=void 0},_.flush=function(){return void 0===a?s:E(h())},_}function y(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function w(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&m.call(e)==i}(e))return o;if(y(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=y(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(r,"");var n=s.test(e);return n||a.test(e)?u(e.slice(2),n?2:8):c.test(e)?o:+e}e.exports=function(e,t,o){var i=!0,r=!0;if("function"!=typeof e)throw new TypeError(n);return y(o)&&(i="leading"in o?!!o.leading:i,r="trailing"in o?!!o.trailing:r),v(e,t,{leading:i,maxWait:t,trailing:r})}}).call(this,n(1))}]); \ No newline at end of file +!function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=3)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSettings=t.getData=void 0;let o=void 0;function i(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error("Could not load data for "+e)}t.getData=i,t.getSettings=function(){if(o)return o;if(o=i("data-settings"),o)return o;throw new Error("Could not load settings")}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getLineElementForFragment=t.getEditorLineNumberForPageOffset=t.scrollToRevealSourceLine=t.getLineElementsAtPageOffset=t.getElementsForSourceLine=void 0;const o=n(0);function i(e){return t=0,n=o.getSettings().lineCount-1,i=e,Math.min(n,Math.max(t,i));var t,n,i}const r=(()=>{let e;return()=>{if(!e){e=[{element:document.body,line:0}];for(const t of document.getElementsByClassName("code-line")){const n=+t.getAttribute("data-line");isNaN(n)||("CODE"===t.tagName&&t.parentElement&&"PRE"===t.parentElement.tagName?e.push({element:t.parentElement,line:n}):e.push({element:t,line:n}))}}return e}})();function s(e){const t=Math.floor(e),n=r();let o=n[0]||null;for(const e of n){if(e.line===t)return{previous:e,next:void 0};if(e.line>t)return{previous:o,next:e};o=e}return{previous:o}}function a(e){const t=r(),n=e-window.scrollY;let o=-1,i=t.length-1;for(;o+1=n?i=e:o=e}const s=t[i],a=c(s);if(i>=1&&a.top>n){return{previous:t[o],next:s}}return i>1&&in?{previous:s,next:t[i+1]}:{previous:s}}function c({element:e}){const t=e.getBoundingClientRect(),n=e.querySelector(".code-line");if(n){const e=n.getBoundingClientRect(),o=Math.max(1,e.top-t.top);return{top:t.top,height:o}}return t}t.getElementsForSourceLine=s,t.getLineElementsAtPageOffset=a,t.scrollToRevealSourceLine=function(e){if(!o.getSettings().scrollPreviewWithEditor)return;if(e<=0)return void window.scroll(window.scrollX,0);const{previous:t,next:n}=s(e);if(!t)return;let i=0;const r=c(t),a=r.top;if(n&&n.line!==t.line){i=a+(e-t.line)/(n.line-t.line)*(n.element.getBoundingClientRect().top-a)}else{const t=e-Math.floor(e);i=a+r.height*t}i=Math.abs(i)<1?Math.sign(i):i,window.scroll(window.scrollX,Math.max(1,window.scrollY+i))},t.getEditorLineNumberForPageOffset=function(e){const{previous:t,next:n}=a(e);if(t){const o=c(t),r=e-window.scrollY-o.top;if(n){const e=r/(c(n).top-o.top);return i(t.line+e*(n.line-t.line))}{const e=r/o.height;return i(t.line+e)}}return null},t.getLineElementForFragment=function(e){return r().find(t=>t.element.id===e)}},function(e,t,n){"use strict";(function(e){Object.defineProperty(t,"__esModule",{value:!0});const o=n(7),i=n(8),r=n(9),s=n(2),a=n(0),c=n(10);let u=0;const l=new o.ActiveLineMarker,f=a.getSettings(),d=acquireVsCodeApi(),m=d.getState(),g={..."object"==typeof m?m:{},...a.getData("data-state")};d.setState(g);const p=r.createPosterForVsCode(d);function h(t){const n=document.getElementsByTagName("img");if(n.length>0){const o=Array.from(n).map(e=>e.complete?Promise.resolve():new Promise(t=>{e.addEventListener("load",()=>t()),e.addEventListener("error",()=>t())}));Promise.all(o).then(()=>e(t))}else e(t)}window.cspAlerter.setPoster(p),window.styleLoadingMonitor.setPoster(p),window.onload=()=>{y()},i.onceDocumentLoaded(()=>{const e=g.scrollProgress;"number"!=typeof e||f.fragment?f.scrollPreviewWithEditor&&h(()=>{if(f.fragment){g.fragment=void 0,d.setState(g);const e=s.getLineElementForFragment(f.fragment);e&&(u+=1,s.scrollToRevealSourceLine(e.line))}else isNaN(f.line)||(u+=1,s.scrollToRevealSourceLine(f.line))}):h(()=>{u+=1,window.scrollTo(0,e*document.body.clientHeight)})});const v=(()=>{const e=c(e=>{u+=1,h(()=>s.scrollToRevealSourceLine(e))},50);return t=>{isNaN(t)||(g.line=t,e(t))}})();let y=c(()=>{const e=[];let t=document.getElementsByTagName("img");if(t){let n;for(n=0;n{u+=1,b(),y()},!0),window.addEventListener("message",e=>{if(e.data.source===f.source)switch(e.data.type){case"onDidChangeTextEditorSelection":l.onDidChangeTextEditorSelection(e.data.line);break;case"updateView":v(e.data.line)}},!1),document.addEventListener("dblclick",e=>{if(!f.doubleClickToSwitchToEditor)return;for(let t=e.target;t;t=t.parentNode)if("A"===t.tagName)return;const t=e.pageY,n=s.getEditorLineNumberForPageOffset(t);"number"!=typeof n||isNaN(n)||p.postMessage("didClick",{line:Math.floor(n)})});const w=["http:","https:","mailto:","vscode:","vscode-insiders:"];function b(){g.scrollProgress=window.scrollY/document.body.clientHeight,d.setState(g)}document.addEventListener("click",e=>{if(!e)return;let t=e.target;for(;t;){if(t.tagName&&"A"===t.tagName&&t.href){if(t.getAttribute("href").startsWith("#"))return;let n=t.getAttribute("data-href");if(!n){if(w.some(e=>t.href.startsWith(e)))return;n=t.getAttribute("href")}return/^[a-z\-]+:/i.test(n)?void 0:(p.postMessage("openLink",{href:n}),e.preventDefault(),void e.stopPropagation())}t=t.parentNode}},!0),window.addEventListener("scroll",c(()=>{if(b(),u>0)u-=1;else{const e=s.getEditorLineNumberForPageOffset(window.scrollY);"number"!=typeof e||isNaN(e)||p.postMessage("revealLine",{line:e})}},50))}).call(this,n(4).setImmediate)},function(e,t,n){(function(e){var o=void 0!==e&&e||"undefined"!=typeof self&&self||window,i=Function.prototype.apply;function r(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new r(i.call(setTimeout,o,arguments),clearTimeout)},t.setInterval=function(){return new r(i.call(setInterval,o,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},r.prototype.unref=r.prototype.ref=function(){},r.prototype.close=function(){this._clearFn.call(o,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout((function(){e._onTimeout&&e._onTimeout()}),t))},n(5),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(1))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var o,i,r,s,a,c=1,u={},l=!1,f=e.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(e);d=d&&d.setTimeout?d:e,"[object process]"==={}.toString.call(e.process)?o=function(e){t.nextTick((function(){g(e)}))}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((r=new MessageChannel).port1.onmessage=function(e){g(e.data)},o=function(e){r.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(i=f.documentElement,o=function(e){var t=f.createElement("script");t.onreadystatechange=function(){g(e),t.onreadystatechange=null,i.removeChild(t),t=null},i.appendChild(t)}):o=function(e){setTimeout(g,0,e)}:(s="setImmediate$"+Math.random()+"$",a=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(s)&&g(+t.data.slice(s.length))},e.addEventListener?e.addEventListener("message",a,!1):e.attachEvent("onmessage",a),o=function(t){e.postMessage(s+t,"*")}),d.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n1)for(var n=1;nnew class{postMessage(t,n){e.postMessage({type:t,source:o.getSettings().source,body:n})}}},function(e,t,n){(function(t){var n=/^\s+|\s+$/g,o=/^[-+]0x[0-9a-f]+$/i,i=/^0b[01]+$/i,r=/^0o[0-7]+$/i,s=parseInt,a="object"==typeof t&&t&&t.Object===Object&&t,c="object"==typeof self&&self&&self.Object===Object&&self,u=a||c||Function("return this")(),l=Object.prototype.toString,f=Math.max,d=Math.min,m=function(){return u.Date.now()};function g(e,t,n){var o,i,r,s,a,c,u=0,l=!1,g=!1,v=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function y(t){var n=o,r=i;return o=i=void 0,u=t,s=e.apply(r,n)}function w(e){return u=e,a=setTimeout(T,t),l?y(e):s}function b(e){var n=e-c;return void 0===c||n>=t||n<0||g&&e-u>=r}function T(){var e=m();if(b(e))return E(e);a=setTimeout(T,function(e){var n=t-(e-c);return g?d(n,r-(e-u)):n}(e))}function E(e){return a=void 0,v&&o?y(e):(o=i=void 0,s)}function L(){var e=m(),n=b(e);if(o=arguments,i=this,c=e,n){if(void 0===a)return w(c);if(g)return a=setTimeout(T,t),y(c)}return void 0===a&&(a=setTimeout(T,t)),s}return t=h(t)||0,p(n)&&(l=!!n.leading,r=(g="maxWait"in n)?f(h(n.maxWait)||0,t):r,v="trailing"in n?!!n.trailing:v),L.cancel=function(){void 0!==a&&clearTimeout(a),u=0,o=c=i=a=void 0},L.flush=function(){return void 0===a?s:E(m())},L}function p(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function h(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&"[object Symbol]"==l.call(e)}(e))return NaN;if(p(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=p(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(n,"");var a=i.test(e);return a||r.test(e)?s(e.slice(2),a?2:8):o.test(e)?NaN:+e}e.exports=function(e,t,n){var o=!0,i=!0;if("function"!=typeof e)throw new TypeError("Expected a function");return p(n)&&(o="leading"in n?!!n.leading:o,i="trailing"in n?!!n.trailing:i),g(e,t,{leading:o,maxWait:t,trailing:i})}}).call(this,n(1))}]); \ No newline at end of file diff --git a/extensions/markdown-language-features/media/pre.js b/extensions/markdown-language-features/media/pre.js index 8268f1e2d5..5a2fa42624 100644 --- a/extensions/markdown-language-features/media/pre.js +++ b/extensions/markdown-language-features/media/pre.js @@ -1 +1 @@ -!function(e){var t={};function n(s){if(t[s])return t[s].exports;var o=t[s]={i:s,l:!1,exports:{}};return e[s].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,s){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(s,o,function(t){return e[t]}.bind(null,o));return s},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});let s=void 0;function o(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error(`Could not load data for ${e}`)}t.getData=o,t.getSettings=function(){if(s)return s;if(s=o("data-settings"))return s;throw new Error("Could not load settings")}},,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const s=n(12),o=n(14);window.cspAlerter=new s.CspAlerter,window.styleLoadingMonitor=new o.StyleLoadingMonitor},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const s=n(0),o=n(13);t.CspAlerter=class{constructor(){this.didShow=!1,this.didHaveCspWarning=!1,document.addEventListener("securitypolicyviolation",()=>{this.onCspWarning()}),window.addEventListener("message",e=>{e&&e.data&&"vscode-did-block-svg"===e.data.name&&this.onCspWarning()})}setPoster(e){this.messaging=e,this.didHaveCspWarning&&this.showCspWarning()}onCspWarning(){this.didHaveCspWarning=!0,this.showCspWarning()}showCspWarning(){const e=o.getStrings(),t=s.getSettings();if(this.didShow||t.disableSecurityWarnings||!this.messaging)return;this.didShow=!0;const n=document.createElement("a");n.innerText=e.cspAlertMessageText,n.setAttribute("id","code-csp-warning"),n.setAttribute("title",e.cspAlertMessageTitle),n.setAttribute("role","button"),n.setAttribute("aria-label",e.cspAlertMessageLabel),n.onclick=()=>{this.messaging.postMessage("showPreviewSecuritySelector",{source:t.source})},document.body.appendChild(n)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getStrings=function(){const e=document.getElementById("vscode-markdown-preview-data");if(e){const t=e.getAttribute("data-strings");if(t)return JSON.parse(t)}throw new Error("Could not load strings")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.StyleLoadingMonitor=class{constructor(){this.unloadedStyles=[],this.finishedLoading=!1;const e=e=>{const t=e.target.dataset.source;this.unloadedStyles.push(t)};window.addEventListener("DOMContentLoaded",()=>{for(const t of document.getElementsByClassName("code-user-style"))t.dataset.source&&(t.onerror=e)}),window.addEventListener("load",()=>{this.unloadedStyles.length&&(this.finishedLoading=!0,this.poster&&this.poster.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles}))})}setPoster(e){this.poster=e,this.finishedLoading&&e.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles})}}}]); \ No newline at end of file +!function(e){var t={};function n(o){if(t[o])return t[o].exports;var s=t[o]={i:o,l:!1,exports:{}};return e[o].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(o,s,function(t){return e[t]}.bind(null,s));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSettings=t.getData=void 0;let o=void 0;function s(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error("Could not load data for "+e)}t.getData=s,t.getSettings=function(){if(o)return o;if(o=s("data-settings"),o)return o;throw new Error("Could not load settings")}},,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(12),s=n(14);window.cspAlerter=new o.CspAlerter,window.styleLoadingMonitor=new s.StyleLoadingMonitor},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CspAlerter=void 0;const o=n(0),s=n(13);t.CspAlerter=class{constructor(){this.didShow=!1,this.didHaveCspWarning=!1,document.addEventListener("securitypolicyviolation",()=>{this.onCspWarning()}),window.addEventListener("message",e=>{e&&e.data&&"vscode-did-block-svg"===e.data.name&&this.onCspWarning()})}setPoster(e){this.messaging=e,this.didHaveCspWarning&&this.showCspWarning()}onCspWarning(){this.didHaveCspWarning=!0,this.showCspWarning()}showCspWarning(){const e=s.getStrings(),t=o.getSettings();if(this.didShow||t.disableSecurityWarnings||!this.messaging)return;this.didShow=!0;const n=document.createElement("a");n.innerText=e.cspAlertMessageText,n.setAttribute("id","code-csp-warning"),n.setAttribute("title",e.cspAlertMessageTitle),n.setAttribute("role","button"),n.setAttribute("aria-label",e.cspAlertMessageLabel),n.onclick=()=>{this.messaging.postMessage("showPreviewSecuritySelector",{source:t.source})},document.body.appendChild(n)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getStrings=void 0,t.getStrings=function(){const e=document.getElementById("vscode-markdown-preview-data");if(e){const t=e.getAttribute("data-strings");if(t)return JSON.parse(t)}throw new Error("Could not load strings")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.StyleLoadingMonitor=void 0;t.StyleLoadingMonitor=class{constructor(){this.unloadedStyles=[],this.finishedLoading=!1;const e=e=>{const t=e.target.dataset.source;this.unloadedStyles.push(t)};window.addEventListener("DOMContentLoaded",()=>{for(const t of document.getElementsByClassName("code-user-style"))t.dataset.source&&(t.onerror=e)}),window.addEventListener("load",()=>{this.unloadedStyles.length&&(this.finishedLoading=!0,this.poster&&this.poster.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles}))})}setPoster(e){this.poster=e,this.finishedLoading&&e.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles})}}}]); \ No newline at end of file diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 4c6bfc998d..c70a9ce62b 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -325,17 +325,17 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "highlight.js": "10.4.1", - "markdown-it": "^10.0.0", + "highlight.js": "^10.4.1", + "markdown-it": "^12.0.3", "markdown-it-front-matter": "^0.2.1", "vscode-extension-telemetry": "0.1.1", "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/highlight.js": "9.12.3", + "@types/highlight.js": "10.1.0", "@types/lodash.throttle": "^4.1.3", "@types/markdown-it": "0.0.2", - "@types/node": "^12.11.7", + "@types/node": "^12.19.9", "lodash.throttle": "^4.1.1", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 69731ba891..4674b7ad80 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -12,7 +12,7 @@ import throttle = require('lodash.throttle'); declare let acquireVsCodeApi: any; -let scrollDisabled = true; +let scrollDisabledCount = 0; const marker = new ActiveLineMarker(); const settings = getSettings(); @@ -37,19 +37,39 @@ window.onload = () => { updateImageSizes(); }; + +function doAfterImagesLoaded(cb: () => void) { + const imgElements = document.getElementsByTagName('img'); + if (imgElements.length > 0) { + const ps = Array.from(imgElements).map(e => { + if (e.complete) { + return Promise.resolve(); + } else { + return new Promise((resolve) => { + e.addEventListener('load', () => resolve()); + e.addEventListener('error', () => resolve()); + }); + } + }); + Promise.all(ps).then(() => setImmediate(cb)); + } else { + setImmediate(cb); + } +} + onceDocumentLoaded(() => { const scrollProgress = state.scrollProgress; if (typeof scrollProgress === 'number' && !settings.fragment) { - setImmediate(() => { - scrollDisabled = true; + doAfterImagesLoaded(() => { + scrollDisabledCount += 1; window.scrollTo(0, scrollProgress * document.body.clientHeight); }); return; } if (settings.scrollPreviewWithEditor) { - setImmediate(() => { + doAfterImagesLoaded(() => { // Try to scroll to fragment if available if (settings.fragment) { state.fragment = undefined; @@ -57,12 +77,12 @@ onceDocumentLoaded(() => { const element = getLineElementForFragment(settings.fragment); if (element) { - scrollDisabled = true; + scrollDisabledCount += 1; scrollToRevealSourceLine(element.line); } } else { if (!isNaN(settings.line!)) { - scrollDisabled = true; + scrollDisabledCount += 1; scrollToRevealSourceLine(settings.line!); } } @@ -72,8 +92,8 @@ onceDocumentLoaded(() => { const onUpdateView = (() => { const doScroll = throttle((line: number) => { - scrollDisabled = true; - scrollToRevealSourceLine(line); + scrollDisabledCount += 1; + doAfterImagesLoaded(() => scrollToRevealSourceLine(line)); }, 50); return (line: number) => { @@ -109,7 +129,7 @@ let updateImageSizes = throttle(() => { }, 50); window.addEventListener('resize', () => { - scrollDisabled = true; + scrollDisabledCount += 1; updateScrollProgress(); updateImageSizes(); }, true); @@ -189,8 +209,8 @@ document.addEventListener('click', event => { window.addEventListener('scroll', throttle(() => { updateScrollProgress(); - if (scrollDisabled) { - scrollDisabled = false; + if (scrollDisabledCount > 0) { + scrollDisabledCount -= 1; } else { const line = getEditorLineNumberForPageOffset(window.scrollY); if (typeof line === 'number' && !isNaN(line)) { diff --git a/extensions/markdown-language-features/preview-src/scroll-sync.ts b/extensions/markdown-language-features/preview-src/scroll-sync.ts index f7a97f6a42..4e2e533e1d 100644 --- a/extensions/markdown-language-features/preview-src/scroll-sync.ts +++ b/extensions/markdown-language-features/preview-src/scroll-sync.ts @@ -143,6 +143,7 @@ export function scrollToRevealSourceLine(line: number) { const progressInElement = line - Math.floor(line); scrollTo = previousTop + (rect.height * progressInElement); } + scrollTo = Math.abs(scrollTo) < 1 ? Math.sign(scrollTo) : scrollTo; window.scroll(window.scrollX, Math.max(1, window.scrollY + scrollTo)); } diff --git a/extensions/markdown-language-features/src/commands/renderDocument.ts b/extensions/markdown-language-features/src/commands/renderDocument.ts index 630e7cdf52..3e1f535d21 100644 --- a/extensions/markdown-language-features/src/commands/renderDocument.ts +++ b/extensions/markdown-language-features/src/commands/renderDocument.ts @@ -15,6 +15,6 @@ export class RenderDocument implements Command { ) { } public async execute(document: SkinnyTextDocument | string): Promise { - return this.engine.render(document); + return (await (this.engine.render(document))).html; } } diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index e1f03763e2..fc0d7fd1ad 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -14,8 +14,9 @@ import { isMarkdownFile } from '../util/file'; import { normalizeResource, WebviewResourceProvider } from '../util/resources'; import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; -import { MarkdownContentProvider } from './previewContentProvider'; +import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; import { MarkdownEngine } from '../markdownEngine'; +import { urlToUri } from '../util/url'; const localize = nls.loadMessageBundle(); @@ -90,7 +91,7 @@ class StartingScrollLine { ) { } } -class StartingScrollFragment { +export class StartingScrollFragment { public readonly type = 'fragment'; constructor( @@ -118,6 +119,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private _disposed: boolean = false; private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = []; + private readonly _fileWatchersBySrc = new Map(); + constructor( webview: vscode.WebviewPanel, resource: vscode.Uri, @@ -156,6 +159,16 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } })); + const watcher = this._register(vscode.workspace.createFileSystemWatcher(resource.fsPath)); + this._register(watcher.onDidChange(uri => { + if (this.isPreviewOf(uri)) { + // Only use the file system event when VS Code does not already know about the file + if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() !== uri.toString())) { + this.refresh(); + } + } + })); + this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { if (e.source !== this._resource.toString()) { return; @@ -198,6 +211,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { super.dispose(); this._disposed = true; clearTimeout(this.throttleTimer); + for (const entry of this._fileWatchersBySrc.values()) { + entry.dispose(); + } } public get resource(): vscode.Uri { @@ -214,6 +230,10 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { }; } + /** + * The first call immediately refreshes the preview, + * calls happening shortly thereafter are debounced. + */ public refresh() { // Schedule update if none is pending if (!this.throttleTimer) { @@ -350,7 +370,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource); } - private setContent(html: string): void { + private setContent(content: MarkdownContentProviderOutput): void { if (this._disposed) { return; } @@ -361,7 +381,30 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { this._webviewPanel.iconPath = this.iconPath; this._webviewPanel.webview.options = this.getWebviewOptions(); - this._webviewPanel.webview.html = html; + this._webviewPanel.webview.html = content.html; + + const srcs = new Set(content.containingImages.map(img => img.src)); + + // Delete stale file watchers. + for (const [src, watcher] of [...this._fileWatchersBySrc]) { + if (!srcs.has(src)) { + watcher.dispose(); + this._fileWatchersBySrc.delete(src); + } + } + + // Create new file watchers. + const root = vscode.Uri.joinPath(this._resource, '../'); + for (const src of srcs) { + const uri = urlToUri(src, root); + if (uri && uri.scheme === 'file' && !this._fileWatchersBySrc.has(src)) { + const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath); + watcher.onDidChange(() => { + this.refresh(); + }); + this._fileWatchersBySrc.set(src, watcher); + } + } } private getWebviewOptions(): vscode.WebviewOptions { diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 49e3695e58..bb471d814d 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -39,6 +39,12 @@ function escapeAttribute(value: string | vscode.Uri): string { return value.toString().replace(/"/g, '"'); } +export interface MarkdownContentProviderOutput { + html: string; + containingImages: { src: string }[]; +} + + export class MarkdownContentProvider { constructor( private readonly engine: MarkdownEngine, @@ -54,11 +60,12 @@ export class MarkdownContentProvider { previewConfigurations: MarkdownPreviewConfigurationManager, initialLine: number | undefined = undefined, state?: any - ): Promise { + ): Promise { const sourceUri = markdownDocument.uri; const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { source: sourceUri.toString(), + fragment: state?.fragment || markdownDocument.uri.fragment || undefined, line: initialLine, lineCount: markdownDocument.lineCount, scrollPreviewWithEditor: config.scrollPreviewWithEditor, @@ -75,7 +82,7 @@ export class MarkdownContentProvider { const csp = this.getCsp(resourceProvider, sourceUri, nonce); const body = await this.engine.render(markdownDocument); - return ` + const html = ` @@ -89,11 +96,15 @@ export class MarkdownContentProvider { - ${body} + ${body.html}
${this.getScripts(resourceProvider, nonce)} `; + return { + html, + containingImages: body.containingImages, + }; } public provideFileNotFoundContent( diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 77d8ef2bc4..cb96049a3b 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -9,7 +9,7 @@ import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable, disposeAll } from '../util/dispose'; import { TopmostLineMonitor } from '../util/topmostLineMonitor'; -import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview'; +import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview } from './preview'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider } from './previewContentProvider'; @@ -106,7 +106,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview preview = this.createNewDynamicPreview(resource, settings); } - preview.update(resource); + preview.update( + resource, + resource.fragment ? new StartingScrollFragment(resource.fragment) : undefined + ); } public get activePreviewResource() { diff --git a/extensions/markdown-language-features/src/features/smartSelect.ts b/extensions/markdown-language-features/src/features/smartSelect.ts index 2cf199a662..0ad3e90f60 100644 --- a/extensions/markdown-language-features/src/features/smartSelect.ts +++ b/extensions/markdown-language-features/src/features/smartSelect.ts @@ -34,7 +34,7 @@ export default class MarkdownSmartSelect implements vscode.SelectionRangeProvide const tokens = await this.engine.parse(document); - const blockTokens = getBlockTokensForPosition(tokens, position); + const blockTokens = getBlockTokensForPosition(tokens, position, headerRange); if (blockTokens.length === 0) { return undefined; @@ -91,13 +91,13 @@ function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, // of this header then all content then header return new vscode.SelectionRange(contentRange.with(undefined, startOfChildRange), new vscode.SelectionRange(contentRange, (new vscode.SelectionRange(range, parent)))); } else { - // no children and not on this header line so select content then header + // not on this header line so select content then header return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent)); } } -function getBlockTokensForPosition(tokens: Token[], position: vscode.Position): Token[] { - const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && isBlockElement(token)); +function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): Token[] { + const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token)); if (enclosingTokens.length === 0) { return []; } @@ -131,9 +131,17 @@ function createInlineRange(document: vscode.TextDocument, cursorPosition: vscode const lineText = document.lineAt(cursorPosition.line).text; const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent); const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent); - const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, boldSelection ? boldSelection : italicSelection || parent); + let comboSelection: vscode.SelectionRange | undefined; + if (boldSelection && italicSelection && !boldSelection.range.isEqual(italicSelection.range)) { + if (boldSelection.range.contains(italicSelection.range)) { + comboSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, boldSelection); + } else if (italicSelection.range.contains(boldSelection.range)) { + comboSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, italicSelection); + } + } + const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, comboSelection || boldSelection || italicSelection || parent); const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection || parent); - return inlineCodeBlockSelection || linkSelection || boldSelection || italicSelection; + return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection; } function createFencedRange(token: Token, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange { @@ -154,61 +162,40 @@ function createFencedRange(token: Token, cursorLine: number, document: vscode.Te } function createBoldRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { - // find closest ** that occurs before cursor position - let startBold = lineText.substring(0, cursorChar).lastIndexOf('**'); - - // find closest ** that occurs after the start ** - const endBoldIndex = lineText.substring(startBold + 2).indexOf('**'); - let endBold = startBold + 2 + lineText.substring(startBold + 2).indexOf('**'); - - if (startBold >= 0 && endBoldIndex >= 0 && startBold + 1 < endBold && startBold <= cursorChar && endBold >= cursorChar) { - const range = new vscode.Range(cursorLine, startBold, cursorLine, endBold + 2); - // **content cursor content** so select content then ** on both sides - const contentRange = new vscode.Range(cursorLine, startBold + 2, cursorLine, endBold); - return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent)); - } else if (startBold >= 0) { - // **content**cursor or **content*cursor* - // find end ** from end of start ** to end of line (since the cursor is within the end stars) - let adjustedEnd = startBold + 2 + lineText.substring(startBold + 2).indexOf('**'); - startBold = lineText.substring(0, adjustedEnd - 2).lastIndexOf('**'); - if (adjustedEnd >= 0 && cursorChar === adjustedEnd || cursorChar === adjustedEnd + 1) { - if (lineText.charAt(adjustedEnd + 1) === '*') { - // *cursor* so need to extend end to include the second * - adjustedEnd += 1; - } - return new vscode.SelectionRange(new vscode.Range(cursorLine, startBold, cursorLine, adjustedEnd + 1), parent); - } - } else if (endBold > 0) { - // cursor**content** or *cursor*content** - // find start ** from start of string to cursor + 2 (since the cursor is within the start stars) - const adjustedStart = lineText.substring(0, cursorChar + 2).lastIndexOf('**'); - endBold = adjustedStart + 2 + lineText.substring(adjustedStart + 2).indexOf('**'); - if (adjustedStart >= 0 && adjustedStart === cursorChar || adjustedStart === cursorChar - 1) { - return new vscode.SelectionRange(new vscode.Range(cursorLine, adjustedStart, cursorLine, endBold + 2), parent); - } + const regex = /(?:\*\*([^*]+)(?:\*([^*]+)([^*]+)\*)*([^*]+)\*\*)/g; + const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar); + if (matches.length) { + // should only be one match, so select first and index 0 contains the entire match + const bold = matches[0][0]; + const startIndex = lineText.indexOf(bold); + const cursorOnStars = cursorChar === startIndex || cursorChar === startIndex + 1 || cursorChar === startIndex + bold.length || cursorChar === startIndex + bold.length - 1; + const contentAndStars = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + bold.length), parent); + const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 2, cursorLine, startIndex + bold.length - 2), contentAndStars); + return cursorOnStars ? contentAndStars : content; } return undefined; } function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine: number, isItalic: boolean, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { - const type = isItalic ? '*' : '`'; - const start = lineText.substring(0, cursorChar + 1).lastIndexOf(type); - let end = lineText.substring(cursorChar).indexOf(type); - - if (start >= 0 && end >= 0) { - end += cursorChar; - // ensure there's no * or ` before end - const intermediate = lineText.substring(start + 1, end - 1).indexOf(type); - if (intermediate < 0) { - const range = new vscode.Range(cursorLine, start, cursorLine, end + 1); - if (cursorChar > start && cursorChar <= end) { - // within the content so select content then include the stars or backticks - const contentRange = new vscode.Range(cursorLine, start + 1, cursorLine, end); - return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent)); - } else if (cursorChar === start) { - return new vscode.SelectionRange(range, parent); - } + const italicRegexes = [/(?:[^*]+)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]+)/g, /^(?:[^*]*)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]*)$/g]; + let matches = []; + if (isItalic) { + matches = [...lineText.matchAll(italicRegexes[0])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar); + if (!matches.length) { + matches = [...lineText.matchAll(italicRegexes[1])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar); } + } else { + matches = [...lineText.matchAll(/\`[^\`]*\`/g)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar); + } + if (matches.length) { + // should only be one match, so select first and select group 1 for italics because that contains just the italic section + // doesn't include the leading and trailing characters which are guaranteed to not be * so as not to be confused with bold + const match = isItalic ? matches[0][1] : matches[0][0]; + const startIndex = lineText.indexOf(match); + const cursorOnType = cursorChar === startIndex || cursorChar === startIndex + match.length; + const contentAndType = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + match.length), parent); + const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 1, cursorLine, startIndex + match.length - 1), contentAndType); + return cursorOnType ? contentAndType : content; } return undefined; } @@ -217,7 +204,7 @@ function createLinkRange(lineText: string, cursorChar: number, cursorLine: numbe const regex = /(\[[^\(\)]*\])(\([^\[\]]*\))/g; const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length > cursorChar); - if (matches.length > 0) { + if (matches.length) { // should only be one match, so select first and index 0 contains the entire match, so match = [text](url) const link = matches[0][0]; const linkRange = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(link), cursorLine, lineText.indexOf(link) + link.length), parent); @@ -228,11 +215,12 @@ function createLinkRange(lineText: string, cursorChar: number, cursorLine: numbe // determine if cursor is within [text] or (url) in order to know which should be selected const nearestType = cursorChar >= lineText.indexOf(linkText) && cursorChar < lineText.indexOf(linkText) + linkText.length ? linkText : url; + const indexOfType = lineText.indexOf(nearestType); // determine if cursor is on a bracket or paren and if so, return the [content] or (content), skipping over the content range - const cursorOnType = cursorChar === lineText.indexOf(nearestType) || cursorChar === lineText.indexOf(nearestType) + nearestType.length; + const cursorOnType = cursorChar === indexOfType || cursorChar === indexOfType + nearestType.length; - const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(nearestType), cursorLine, lineText.indexOf(nearestType) + nearestType.length), linkRange); - const content = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(nearestType) + 1, cursorLine, lineText.indexOf(nearestType) + nearestType.length - 1), contentAndNearestType); + const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType, cursorLine, indexOfType + nearestType.length), linkRange); + const content = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType + 1, cursorLine, indexOfType + nearestType.length - 1), contentAndNearestType); return cursorOnType ? contentAndNearestType : content; } return undefined; diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index fbf3051a54..0e0849f142 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -54,6 +54,15 @@ class TokenCache { } } +export interface RenderOutput { + html: string; + containingImages: { src: string }[]; +} + +interface RenderEnv { + containingImages: { src: string }[]; +} + export class MarkdownEngine { private md?: Promise; @@ -148,7 +157,7 @@ export class MarkdownEngine { return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); } - public async render(input: SkinnyTextDocument | string): Promise { + public async render(input: SkinnyTextDocument | string): Promise { const config = this.getConfig(typeof input === 'string' ? undefined : input.uri); const engine = await this.getEngine(config); @@ -156,10 +165,19 @@ export class MarkdownEngine { ? this.tokenizeString(input, engine) : this.tokenizeDocument(input, config, engine); - return engine.renderer.render(tokens, { + const env: RenderEnv = { + containingImages: [] + }; + + const html = engine.renderer.render(tokens, { ...(engine as any).options, ...config - }, {}); + }, env); + + return { + html, + containingImages: env.containingImages + }; } public async parse(document: SkinnyTextDocument): Promise { @@ -199,12 +217,13 @@ export class MarkdownEngine { private addImageStabilizer(md: any): void { const original = md.renderer.rules.image; - md.renderer.rules.image = (tokens: any, idx: number, options: any, env: any, self: any) => { + md.renderer.rules.image = (tokens: any, idx: number, options: any, env: RenderEnv, self: any) => { const token = tokens[idx]; token.attrJoin('class', 'loading'); const src = token.attrGet('src'); if (src) { + env.containingImages.push({ src }); const imgHash = hash(src); token.attrSet('id', `image-hash-${imgHash}`); } @@ -238,6 +257,13 @@ export class MarkdownEngine { return normalizeLink(vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }).toString()); } + // Support file:// links + if (isOfScheme(Schemes.file, link)) { + // Ensure link is relative by prepending `/` so that it uses the element URI + // when resolving the absolute URL + return normalizeLink('/' + link.replace(/^file:/, 'file')); + } + // If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace if (!/^[a-z\-]+:/i.test(link)) { // Use a fake scheme for parsing @@ -248,12 +274,14 @@ export class MarkdownEngine { if (uri.path[0] === '/') { const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!); if (root) { - const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath); - uri = fileUri.with({ - scheme: uri.scheme, + const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ fragment: uri.fragment, query: uri.query, }); + + // Ensure fileUri is relative by prepending `/` so that it uses the element URI + // when resolving the absolute URL + uri = vscode.Uri.parse('markdown-link:' + '/' + fileUri.toString(true).replace(/^\S+?:/, fileUri.scheme)); } } @@ -276,9 +304,7 @@ export class MarkdownEngine { private addLinkValidator(md: any): void { const validateLink = md.validateLink; md.validateLink = (link: string) => { - // support file:// links return validateLink(link) - || isOfScheme(Schemes.file, link) || isOfScheme(Schemes.vscode, link) || isOfScheme(Schemes['vscode-insiders'], link) || /^data:image\/.*?;/.test(link); diff --git a/extensions/markdown-language-features/src/test/documentLink.test.ts b/extensions/markdown-language-features/src/test/documentLink.test.ts index e86b93cb74..57e011c85d 100644 --- a/extensions/markdown-language-features/src/test/documentLink.test.ts +++ b/extensions/markdown-language-features/src/test/documentLink.test.ts @@ -20,6 +20,11 @@ async function getLinksForFile(file: vscode.Uri): Promise suite('Markdown Document links', () => { + setup(async () => { + // the tests make the assumption that link providers are already registered + await vscode.extensions.getExtension('vscode.markdown-language-features')!.activate(); + }); + teardown(async () => { await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); diff --git a/extensions/markdown-language-features/src/test/engine.test.ts b/extensions/markdown-language-features/src/test/engine.test.ts index 670513267c..e4964dea86 100644 --- a/extensions/markdown-language-features/src/test/engine.test.ts +++ b/extensions/markdown-language-features/src/test/engine.test.ts @@ -21,12 +21,31 @@ suite('markdown.engine', () => { test('Renders a document', async () => { const doc = new InMemoryDocument(testFileName, input); const engine = createNewMarkdownEngine(); - assert.strictEqual(await engine.render(doc), output); + assert.strictEqual((await engine.render(doc)).html, output); }); test('Renders a string', async () => { const engine = createNewMarkdownEngine(); - assert.strictEqual(await engine.render(input), output); + assert.strictEqual((await engine.render(input)).html, output); + }); + }); + + suite('image-caching', () => { + const input = '![](img.png) [](no-img.png) ![](http://example.org/img.png) ![](img.png) ![](./img2.png)'; + + test('Extracts all images', async () => { + const engine = createNewMarkdownEngine(); + assert.deepStrictEqual((await engine.render(input)), { + html: '

' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + + '

\n' + , + containingImages: [{ src: 'img.png' }, { src: 'http://example.org/img.png' }, { src: 'img.png' }, { src: './img2.png' }], + }); }); }); }); diff --git a/extensions/markdown-language-features/src/test/smartSelect.test.ts b/extensions/markdown-language-features/src/test/smartSelect.test.ts index bb96c903d1..d35a4196ff 100644 --- a/extensions/markdown-language-features/src/test/smartSelect.test.ts +++ b/extensions/markdown-language-features/src/test/smartSelect.test.ts @@ -520,10 +520,10 @@ suite('markdown.SmartSelect', () => { `paragraph`, `## sub header`, `- list`, - `- stuff here [text]**${CURSOR}items in here** and **here**`, + `- stuff here [text] **${CURSOR}items in here** and **here**`, `- list` )); - assertNestedRangesEqual(ranges![0], [6, 21, 6, 44], [6, 19, 6, 46], [6, 0, 6, 59], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]); + assertNestedRangesEqual(ranges![0], [6, 22, 6, 45], [6, 20, 6, 47], [6, 0, 6, 60], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]); }); test('Smart select link in paragraph with multiple links', async () => { const ranges = await getSelectionRangesForDocument( @@ -567,11 +567,69 @@ suite('markdown.SmartSelect', () => { )); assertNestedRangesEqual(ranges![0], [0, 2, 0, 21], [0, 1, 0, 22], [0, 1, 0, 42], [0, 1, 0, 42], [0, 0, 0, 43], [0, 0, 0, 43]); }); + test('Smart select italic on end', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `*word1 word2 word3${CURSOR}*` + )); + assertNestedRangesEqual(ranges![0], [0, 1, 0, 28], [0, 0, 0, 29], [0, 0, 0, 29]); + }); + test('Smart select italic then bold', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `outer text **bold words *italic ${CURSOR} words* bold words** outer text` + )); + assertNestedRangesEqual(ranges![0], [0, 25, 0, 48], [0, 24, 0, 49], [0, 13, 0, 60], [0, 11, 0, 62], [0, 0, 0, 73]); + }); + test('Smart select bold then italic', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `outer text *italic words **bold ${CURSOR} words** italic words* outer text` + )); + assertNestedRangesEqual(ranges![0], [0, 27, 0, 48], [0, 25, 0, 50], [0, 12, 0, 63], [0, 11, 0, 64], [0, 0, 0, 75]); + }); + test('Third level header from release notes', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `---`, + `Order: 60`, + `TOCTitle: October 2020`, + `PageTitle: Visual Studio Code October 2020`, + `MetaDescription: Learn what is new in the Visual Studio Code October 2020 Release (1.51)`, + `MetaSocialImage: 1_51/release-highlights.png`, + `Date: 2020-11-6`, + `DownloadVersion: 1.51.1`, + `---`, + `# October 2020 (version 1.51)`, + ``, + `**Update 1.51.1**: The update addresses these [issues](https://github.com/microsoft/vscode/issues?q=is%3Aissue+milestone%3A%22October+2020+Recovery%22+is%3Aclosed+).`, + ``, + ``, + ``, + `---`, + ``, + `Welcome to the October 2020 release of Visual Studio Code. As announced in the [October iteration plan](https://github.com/microsoft/vscode/issues/108473), we focused on housekeeping GitHub issues and pull requests as documented in our issue grooming guide.`, + ``, + `We also worked with our partners at GitHub on GitHub Codespaces, which ended up being more involved than originally anticipated. To that end, we'll continue working on housekeeping for part of the November iteration.`, + ``, + `During this housekeeping milestone, we also addressed several feature requests and community [pull requests](#thank-you). Read on to learn about new features and settings.`, + ``, + `## Workbench`, + ``, + `### More prominent pinned tabs`, + ``, + `${CURSOR}Pinned tabs will now always show their pin icon, even while inactive, to make them easier to identify. If an editor is both pinned and contains unsaved changes, the icon reflects both states.`, + ``, + `![Inactive pinned tabs showing pin icons](images/1_51/pinned-tabs.png)` + ) + ); + assertNestedRangesEqual(ranges![0], [27, 0, 27, 201], [26, 0, 29, 70], [25, 0, 29, 70], [24, 0, 29, 70], [23, 0, 29, 70], [10, 0, 29, 70], [9, 0, 29, 70]); + }); }); function assertNestedLineNumbersEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number][]) { const lineage = getLineage(range); - assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length}`); + assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length} ${getValues(lineage)}`); for (let i = 0; i < lineage.length; i++) { assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`); } diff --git a/extensions/markdown-language-features/src/test/urlToUri.test.ts b/extensions/markdown-language-features/src/test/urlToUri.test.ts new file mode 100644 index 0000000000..b80cb88ec8 --- /dev/null +++ b/extensions/markdown-language-features/src/test/urlToUri.test.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual } from 'assert'; +import 'mocha'; +import { Uri } from 'vscode'; +import { urlToUri } from '../util/url'; + +suite('urlToUri', () => { + test('Absolute File', () => { + deepStrictEqual( + urlToUri('file:///root/test.txt', Uri.parse('file:///usr/home/')), + Uri.parse('file:///root/test.txt') + ); + }); + + test('Relative File', () => { + deepStrictEqual( + urlToUri('./file.ext', Uri.parse('file:///usr/home/')), + Uri.parse('file:///usr/home/file.ext') + ); + }); + + test('Http Basic', () => { + deepStrictEqual( + urlToUri('http://example.org?q=10&f', Uri.parse('file:///usr/home/')), + Uri.parse('http://example.org?q=10&f') + ); + }); + + test('Http Encoded Chars', () => { + deepStrictEqual( + urlToUri('http://example.org/%C3%A4', Uri.parse('file:///usr/home/')), + Uri.parse('http://example.org/%C3%A4') + ); + }); +}); diff --git a/extensions/markdown-language-features/src/util/url.ts b/extensions/markdown-language-features/src/util/url.ts new file mode 100644 index 0000000000..60be6798d4 --- /dev/null +++ b/extensions/markdown-language-features/src/util/url.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +declare const URL: typeof import('url').URL; + +/** + * Tries to convert an url into a vscode uri and returns undefined if this is not possible. + * `url` can be absolute or relative. +*/ +export function urlToUri(url: string, base: vscode.Uri): vscode.Uri | undefined { + try { + // `vscode.Uri.joinPath` cannot be used, since it understands + // `src` as path, not as relative url. This is problematic for query args. + const parsedUrl = new URL(url, base.toString()); + const uri = vscode.Uri.parse(parsedUrl.toString()); + return uri; + } catch (e) { + // Don't crash if `URL` cannot parse `src`. + return undefined; + } +} diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 379a5e3044..305a30435d 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/highlight.js@9.12.3": - version "9.12.3" - resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca" - integrity sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ== +"@types/highlight.js@10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-10.1.0.tgz#89bb0c202997d7a90a07bd2ec1f7d00c56bb90b4" + integrity sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A== + dependencies: + highlight.js "*" "@types/lodash.throttle@^4.1.3": version "4.1.3" @@ -24,10 +26,10 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.20.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.6.tgz#7b73cce37352936e628c5ba40326193443cfba25" + integrity sha512-sRVq8d+ApGslmkE9e3i+D3gFGk7aZHAT+G4cIpIEdLJYPsWiSPwcAnJEjddLQQDqV3Ra2jOclX/Sv6YrvGYiWA== ansi-regex@^3.0.0: version "3.0.0" @@ -43,12 +45,10 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" -argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" - integrity sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY= - dependencies: - sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== charenc@~0.0.1: version "0.0.2" @@ -86,25 +86,25 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -entities@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -highlight.js@10.4.1: - version "10.4.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" - integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== +highlight.js@*, highlight.js@^10.4.1: + version "10.7.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.1.tgz#a8ec4152db24ea630c90927d6cae2a45f8ecb955" + integrity sha512-S6G97tHGqJ/U8DsXcEdnACbirtbx58Bx9CzIVeYli8OuswCfYI/LsXH2EiGcoGio1KAC3x4mmUwulOllJ2ZyRA== is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -linkify-it@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" - integrity sha1-2UpGSPmxwXnWT6lykSaL22zpQ08= +linkify-it@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" + integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ== dependencies: uc.micro "^1.0.1" @@ -123,14 +123,14 @@ markdown-it-front-matter@^0.2.1: resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.1.tgz#dca49a827bb3cebb0528452c1d87dff276eb28dc" integrity sha512-ydUIqlKfDscRpRUTRcA3maeeUKn3Cl5EaKZSA+I/f0KOGCBurW7e+bbz59sxqkC3FA9Q2S2+t4mpkH9T0BCM6A== -markdown-it@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" - integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== +markdown-it@^12.0.3: + version "12.0.4" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" + integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^2.0.0" + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" mdurl "^1.0.1" uc.micro "^1.0.5" @@ -189,11 +189,6 @@ semver@^5.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index d438b96ca6..f53c6ae28e 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -14,7 +14,7 @@ "Other" ], "activationEvents": [ - "*" + "onStartupFinished" ], "main": "./out/mergeConflictMain", "browser": "./dist/browser/mergeConflictMain", @@ -139,6 +139,10 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "^12.11.7" + "@types/node": "^12.19.9" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index 7af62a72ce..687f15f481 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@^12.11.7": - version "12.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" - integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 64becb5046..e6416410be 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -1,59 +1,62 @@ { - "name": "microsoft-authentication", - "publisher": "vscode", - "displayName": "%displayName%", - "description": "%description%", - "version": "0.0.1", - "engines": { - "vscode": "^1.42.0" - }, - "categories": [ - "Other" - ], - "enableProposedApi": true, - "activationEvents": [ - "onAuthenticationRequest:microsoft" - ], - "extensionKind": [ - "ui", - "workspace", - "web" - ], - "contributes": { - "authentication": [ - { - "label": "Microsoft", - "id": "microsoft" - } - ] - }, - "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", - "main": "./out/extension.js", - "browser": "./dist/browser/extension.js", - "scripts": { - "vscode:prepublish": "npm run compile", - "compile": "gulp compile-extension:microsoft-authentication", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch": "gulp watch-extension:microsoft-authentication", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" - }, - "devDependencies": { - "@types/keytar": "^4.0.1", - "@types/node": "^10.12.21", - "@types/node-fetch": "^2.5.7", - "@types/randombytes": "^2.0.0", - "@types/sha.js": "^2.4.0", - "@types/uuid": "^8.0.0", - "typescript": "^3.7.4" - }, - "dependencies": { - "buffer": "^5.6.0", - "node-fetch": "^2.6.0", - "randombytes": "github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971", - "sha.js": "2.4.11", - "stream": "0.0.2", - "uuid": "^8.2.0", - "vscode-extension-telemetry": "0.1.1", - "vscode-nls": "^4.1.1" - } + "name": "microsoft-authentication", + "publisher": "vscode", + "license": "MIT", + "displayName": "%displayName%", + "description": "%description%", + "version": "0.0.1", + "engines": { + "vscode": "^1.42.0" + }, + "categories": [ + "Other" + ], + "enableProposedApi": true, + "activationEvents": [ + "onAuthenticationRequest:microsoft" + ], + "extensionKind": [ + "ui", + "workspace", + "web" + ], + "contributes": { + "authentication": [ + { + "label": "Microsoft", + "id": "microsoft" + } + ] + }, + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "main": "./out/extension.js", + "browser": "./dist/browser/extension.js", + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "gulp compile-extension:microsoft-authentication", + "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", + "watch": "gulp watch-extension:microsoft-authentication", + "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" + }, + "devDependencies": { + "@types/node": "^12.19.9", + "@types/node-fetch": "^2.5.7", + "@types/randombytes": "^2.0.0", + "@types/sha.js": "^2.4.0", + "@types/uuid": "8.0.0" + }, + "dependencies": { + "buffer": "^5.6.0", + "node-fetch": "^2.6.0", + "randombytes": "github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971", + "sha.js": "2.4.11", + "stream": "0.0.2", + "uuid": "^8.2.0", + "vscode-extension-telemetry": "0.1.1", + "vscode-nls": "^4.1.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index eba70287f0..0b51f6c41a 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -10,11 +10,15 @@ import * as vscode from 'vscode'; import { createServer, startServer } from './authServer'; import { v4 as uuid } from 'uuid'; -import { keychain } from './keychain'; +import { Keychain } from './keychain'; import Logger from './logger'; import { toBase64UrlEncoding } from './utils'; -import fetch from 'node-fetch'; +import fetch, { Response } from 'node-fetch'; import { sha256 } from './env/node/sha256'; +import * as nls from 'vscode-nls'; +import { MicrosoftAuthenticationSession } from './microsoft-authentication'; + +const localize = nls.loadMessageBundle(); const redirectUrl = 'https://vscode-redirect.azurewebsites.net/'; const loginEndpointUrl = 'https://login.microsoftonline.com/'; @@ -23,6 +27,7 @@ const tenant = 'organizations'; interface IToken { accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined + idToken?: string; // depending on the scopes can be either supplied or empty expiresIn?: number; // How long access token is valid, in seconds expiresAt?: number; // UNIX epoch time at which token will expire @@ -68,6 +73,11 @@ export interface ITokenResponse { id_token?: string; } +export interface IMicrosoftTokens { + accessToken: string; + idToken?: string; +} + function parseQuery(uri: vscode.Uri) { return uri.query.split('&').reduce((prev: any, current) => { const queryString = current.split('='); @@ -97,13 +107,16 @@ export class AzureActiveDirectoryService { private _codeExchangePromises = new Map>(); private _codeVerfifiers = new Map(); - constructor() { + private _keychain: Keychain; + + constructor(private _context: vscode.ExtensionContext) { + this._keychain = new Keychain(_context); this._uriHandler = new UriEventHandler(); this._disposables.push(vscode.window.registerUriHandler(this._uriHandler)); } public async initialize(): Promise { - const storedData = await keychain.getToken() || await keychain.tryMigrate(); + const storedData = await this._keychain.getToken() || await this._keychain.tryMigrate(); if (storedData) { try { const sessions = this.parseStoredData(storedData); @@ -143,7 +156,7 @@ export class AzureActiveDirectoryService { } } - this._disposables.push(vscode.authentication.onDidChangePassword(() => this.checkForUpdates)); + this._disposables.push(this._context.secrets.onDidChange(() => this.checkForUpdates)); } private parseStoredData(data: string): IStoredSession[] { @@ -160,13 +173,13 @@ export class AzureActiveDirectoryService { }; }); - await keychain.setToken(JSON.stringify(serializedData)); + await this._keychain.setToken(JSON.stringify(serializedData)); } private async checkForUpdates(): Promise { const addedIds: string[] = []; let removedIds: string[] = []; - const storedData = await keychain.getToken(); + const storedData = await this._keychain.getToken(); if (storedData) { try { const sessions = this.parseStoredData(storedData); @@ -222,29 +235,36 @@ export class AzureActiveDirectoryService { } } - private async convertToSession(token: IToken): Promise { - const resolvedToken = await this.resolveAccessToken(token); + private async convertToSession(token: IToken): Promise { + const resolvedTokens = await this.resolveAccessAndIdTokens(token); return { id: token.sessionId, - accessToken: resolvedToken, + accessToken: resolvedTokens.accessToken, + idToken: resolvedTokens.idToken, account: token.account, scopes: token.scope.split(' ') }; } - private async resolveAccessToken(token: IToken): Promise { + private async resolveAccessAndIdTokens(token: IToken): Promise { if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { token.expiresAt ? Logger.info(`Token available from cache, expires in ${token.expiresAt - Date.now()} milliseconds`) : Logger.info('Token available from cache'); - return Promise.resolve(token.accessToken); + return Promise.resolve({ + accessToken: token.accessToken, + idToken: token.idToken + }); } try { Logger.info('Token expired or unavailable, trying refresh'); const refreshedToken = await this.refreshToken(token.refreshToken, token.scope, token.sessionId); if (refreshedToken.accessToken) { - return refreshedToken.accessToken; + return { + accessToken: refreshedToken.accessToken, + idToken: refreshedToken.idToken + }; } else { throw new Error(); } @@ -495,6 +515,7 @@ export class AzureActiveDirectoryService { expiresIn: json.expires_in, expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined, accessToken: json.access_token, + idToken: json.id_token, refreshToken: json.refresh_token, scope, sessionId: existingId || `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}/${uuid()}`, @@ -642,7 +663,7 @@ export class AzureActiveDirectoryService { this.removeInMemorySessionData(sessionId); if (this._tokens.length === 0) { - await keychain.deleteToken(); + await this._keychain.deleteToken(); } else { this.storeTokenData(); } @@ -651,7 +672,7 @@ export class AzureActiveDirectoryService { public async clearSessions() { Logger.info('Logging out of all sessions'); this._tokens = []; - await keychain.deleteToken(); + await this._keychain.deleteToken(); this._refreshTimeouts.forEach(timeout => { clearTimeout(timeout); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 2a2732edb8..9d6223ec6b 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -13,15 +13,12 @@ export async function activate(context: vscode.ExtensionContext) { const { name, version, aiKey } = require('../package.json') as { name: string, version: string, aiKey: string }; const telemetryReporter = new TelemetryReporter(name, version, aiKey); - const loginService = new AzureActiveDirectoryService(); + const loginService = new AzureActiveDirectoryService(context); context.subscriptions.push(loginService); await loginService.initialize(); - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ - id: 'microsoft', - label: 'Microsoft', - supportsMultipleAccounts: true, + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), login: async (scopes: string[]) => { @@ -59,7 +56,7 @@ export async function activate(context: vscode.ExtensionContext) { telemetryReporter.sendTelemetryEvent('logoutFailed'); } } - })); + }, { supportsMultipleAccounts: true })); return; } diff --git a/extensions/microsoft-authentication/src/keychain.ts b/extensions/microsoft-authentication/src/keychain.ts index 3a49b87204..1217c7d55e 100644 --- a/extensions/microsoft-authentication/src/keychain.ts +++ b/extensions/microsoft-authentication/src/keychain.ts @@ -35,7 +35,7 @@ const ACCOUNT_ID = 'account'; export class Keychain { private keytar: Keytar; - constructor() { + constructor(private context: vscode.ExtensionContext) { const keytar = getKeytar(); if (!keytar) { throw new Error('System keychain unavailable'); @@ -46,8 +46,9 @@ export class Keychain { async setToken(token: string): Promise { + try { - return await vscode.authentication.setPassword(SERVICE_ID, token); + return await this.context.secrets.store(SERVICE_ID, token); } catch (e) { Logger.error(`Setting token failed: ${e}`); @@ -69,7 +70,7 @@ export class Keychain { async getToken(): Promise { try { - return await vscode.authentication.getPassword(SERVICE_ID); + return await this.context.secrets.get(SERVICE_ID); } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); @@ -79,7 +80,7 @@ export class Keychain { async deleteToken(): Promise { try { - return await vscode.authentication.deletePassword(SERVICE_ID); + return await this.context.secrets.delete(SERVICE_ID); } catch (e) { // Ignore Logger.error(`Deleting token failed: ${e}`); @@ -102,5 +103,3 @@ export class Keychain { } } } - -export const keychain = new Keychain(); diff --git a/extensions/microsoft-authentication/src/microsoft-authentication.d.ts b/extensions/microsoft-authentication/src/microsoft-authentication.d.ts new file mode 100644 index 0000000000..167d789314 --- /dev/null +++ b/extensions/microsoft-authentication/src/microsoft-authentication.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AuthenticationSession } from 'vscode'; + +/** + * Represents a session of a currently logged in Microsoft user. + */ +export interface MicrosoftAuthenticationSession extends AuthenticationSession { + /** + * The id token. + */ + idToken?: string; +} \ No newline at end of file diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index df970ce40d..58e34ee843 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -2,13 +2,6 @@ # yarn lockfile v1 -"@types/keytar@^4.0.1": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.2.tgz#49ef917d6cbb4f19241c0ab50cd35097b5729b32" - integrity sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw== - dependencies: - keytar "*" - "@types/node-fetch@^2.5.7": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" @@ -22,10 +15,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@^10.12.21": - version "10.17.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" - integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== +"@types/node@^12.19.9": + version "12.19.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" + integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== "@types/randombytes@^2.0.0": version "2.0.0" @@ -41,21 +34,11 @@ dependencies: "@types/node" "*" -"@types/uuid@^8.0.0": +"@types/uuid@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -65,19 +48,6 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -88,13 +58,6 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== - dependencies: - readable-stream "^3.0.1" - buffer@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -103,16 +66,6 @@ buffer@^5.6.0: base64-js "^1.0.2" ieee754 "^1.1.4" -chownr@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" - integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -120,43 +73,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== - dependencies: - mimic-response "^2.0.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" @@ -174,18 +95,6 @@ emitter-component@^1.1.1: resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6" integrity sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY= -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -195,75 +104,16 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -keytar@*: - version "5.0.0" - resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.0.0.tgz#c89b6b7a4608fd7af633d9f8474b1a7eb97cbe6f" - integrity sha512-a5UheK59YOlJf9i+2Osaj/kkH6mK0RCHVMtJ84u6ZfbfRIbOJ/H4b5VlOF/LgNHF6s78dRSBzZnvIuPiBKv6wg== - dependencies: - nan "2.14.0" - prebuild-install "5.3.3" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -276,155 +126,18 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -mimic-response@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" - integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -nan@2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -napi-build-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508" - integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA== - -node-abi@^2.7.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63" - integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA== - dependencies: - semver "^5.4.1" - 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== -noop-logger@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" - integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= - -npmlog@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -prebuild-install@5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e" - integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.0" - mkdirp "^0.5.1" - napi-build-utils "^1.0.1" - node-abi "^2.7.0" - noop-logger "^0.1.1" - npmlog "^4.0.1" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^3.0.3" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - which-pm-runs "^1.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - "randombytes@github:rmacfarlane/randombytes#b28d4ecee46262801ea09f15fa1f1513a05c5971": version "2.1.0" resolved "https://codeload.github.com/rmacfarlane/randombytes/tar.gz/b28d4ecee46262801ea09f15fa1f1513a05c5971" dependencies: safe-buffer "^5.1.0" -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -434,21 +147,11 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -semver@^5.3.0, semver@^5.4.1: +semver@^5.3.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - sha.js@2.4.11: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -457,25 +160,6 @@ sha.js@2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -simple-concat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" - integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= - -simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - stream@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef" @@ -483,94 +167,6 @@ stream@0.0.2: dependencies: emitter-component "^1.1.1" -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -tar-fs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" - integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== - dependencies: - chownr "^1.1.1" - mkdirp "^0.5.1" - pump "^3.0.0" - tar-stream "^2.0.0" - -tar-stream@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" - integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== - dependencies: - bl "^3.0.0" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -typescript@^3.7.4: - version "3.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" - integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - uuid@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e" @@ -588,23 +184,6 @@ vscode-nls@^4.1.1: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== -which-pm-runs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" - integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - zone.js@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" diff --git a/extensions/mssql/.vscodeignore b/extensions/mssql/.vscodeignore index edaee711fa..2fc8525ad0 100644 --- a/extensions/mssql/.vscodeignore +++ b/extensions/mssql/.vscodeignore @@ -5,8 +5,8 @@ extension.webpack.config.js yarn.lock .vscode node_modules -!node_modules/ads-kerberos/package.json -!node_modules/ads-kerberos/LICENSE -!node_modules/ads-kerberos/lib -!node_modules/ads-kerberos/index.js -!node_modules/ads-kerberos/build/Release/kerberos.node +!node_modules/ads-kerberos2/package.json +!node_modules/ads-kerberos2/LICENSE +!node_modules/ads-kerberos2/lib +!node_modules/ads-kerberos2/index.js +!node_modules/ads-kerberos2/build/Release/kerberos.node diff --git a/extensions/mssql/extension.webpack.config.js b/extensions/mssql/extension.webpack.config.js index afb59e24ed..f07dd73378 100644 --- a/extensions/mssql/extension.webpack.config.js +++ b/extensions/mssql/extension.webpack.config.js @@ -15,6 +15,6 @@ module.exports = withDefaults({ main: './src/main.ts' }, externals: { - 'ads-kerberos': 'commonjs ads-kerberos' + 'ads-kerberos2': 'commonjs ads-kerberos2' } }); diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 653874275b..0813bda632 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -1256,7 +1256,7 @@ } }, "dependencies": { - "ads-kerberos": "^1.1.3", + "ads-kerberos2": "^1.1.3", "buffer-stream-reader": "^0.1.1", "bytes": "^3.1.0", "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.2.2", diff --git a/extensions/mssql/src/util/auth.ts b/extensions/mssql/src/util/auth.ts index 6da73def10..4d519faa16 100644 --- a/extensions/mssql/src/util/auth.ts +++ b/extensions/mssql/src/util/auth.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as kerberos from 'ads-kerberos'; +import * as kerberos from 'ads-kerberos2'; import * as vscode from 'vscode'; export enum AuthType { diff --git a/extensions/mssql/yarn.lock b/extensions/mssql/yarn.lock index 3590043987..7d5a014a3b 100644 --- a/extensions/mssql/yarn.lock +++ b/extensions/mssql/yarn.lock @@ -236,10 +236,10 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== -ads-kerberos@^1.1.3: +ads-kerberos2@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/ads-kerberos/-/ads-kerberos-1.1.3.tgz#e424253c6f0df2b258aa4a31804f9e38562d0301" - integrity sha512-e6rLQ/ECDd+vo7FhcjbEZPxH3I9cDc/zBj0ih0CH//zZw/a4HI0g/buIuTzU7yRMC5FYEEH9azC82vO6OTbu8g== + resolved "https://registry.yarnpkg.com/ads-kerberos2/-/ads-kerberos2-1.1.3.tgz#2f52c228579c45e7ea975c64070f33d2fdfafb58" + integrity sha512-mSHGHcVuST50I7nekS2gPH9mDDkeoZQagP2mit8Ry2EhAFxXXw/px1wESPe+M60SzSHbXaJ+orV5izoB2Mee9w== dependencies: nan "^2.14.0" diff --git a/extensions/notebook/src/test/common/stubs.ts b/extensions/notebook/src/test/common/stubs.ts index 5501318bd2..edac7abad3 100644 --- a/extensions/notebook/src/test/common/stubs.ts +++ b/extensions/notebook/src/test/common/stubs.ts @@ -31,6 +31,7 @@ export class MockExtensionContext implements vscode.ExtensionContext { globalStorageUri: vscode.Uri; logUri: vscode.Uri; environmentVariableCollection: vscode.EnvironmentVariableCollection; + secrets: vscode.SecretStorage; } export class MockOutputChannel implements vscode.OutputChannel { diff --git a/extensions/package.json b/extensions/package.json index 7abc2e1e6b..5222855bf9 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -1,11 +1,15 @@ { "name": "vscode-extensions", "version": "0.0.1", + "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "4.1.2" + "typescript": "^4.2.0-dev.20210201" }, "scripts": { "postinstall": "node ./postinstall" + }, + "devDependencies": { + "vscode-grammar-updater": "^1.0.2" } } diff --git a/extensions/powershell/package.json b/extensions/powershell/package.json index fb45b704b8..e176f7f412 100644 --- a/extensions/powershell/package.json +++ b/extensions/powershell/package.json @@ -1,30 +1,53 @@ { - "name": "powershell", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "languages": [{ - "id": "powershell", - "extensions": [ ".ps1", ".psm1", ".psd1", ".pssc", ".psrc" ], - "aliases": [ "PowerShell", "powershell", "ps", "ps1" ], - "firstLine": "^#!\\s*/.*\\bpwsh\\b", - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "powershell", - "scopeName": "source.powershell", - "path": "./syntaxes/powershell.tmLanguage.json" - }], - "snippets": [{ - "language": "powershell", - "path": "./snippets/powershell.code-snippets" - }] - }, - "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js PowerShell/EditorSyntax PowerShellSyntax.tmLanguage ./syntaxes/powershell.tmLanguage.json" - } + "name": "powershell", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "languages": [ + { + "id": "powershell", + "extensions": [ + ".ps1", + ".psm1", + ".psd1", + ".pssc", + ".psrc" + ], + "aliases": [ + "PowerShell", + "powershell", + "ps", + "ps1" + ], + "firstLine": "^#!\\s*/.*\\bpwsh\\b", + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "powershell", + "scopeName": "source.powershell", + "path": "./syntaxes/powershell.tmLanguage.json" + } + ], + "snippets": [ + { + "language": "powershell", + "path": "./snippets/powershell.code-snippets" + } + ] + }, + "scripts": { + "update-grammar": "node ../node_modules/.bin/vscode-grammar-updater PowerShell/EditorSyntax PowerShellSyntax.tmLanguage ./syntaxes/powershell.tmLanguage.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/powershell/package.nls.json b/extensions/powershell/package.nls.json index b54b734e59..0316c19e27 100644 --- a/extensions/powershell/package.nls.json +++ b/extensions/powershell/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Powershell Language Basics", "description": "Provides snippets, syntax highlighting, bracket matching and folding in Powershell files." -} \ No newline at end of file +} diff --git a/extensions/powershell/yarn.lock b/extensions/powershell/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/powershell/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/python/.vscodeignore b/extensions/python/.vscodeignore index b5c95d0fb6..fd575df173 100644 --- a/extensions/python/.vscodeignore +++ b/extensions/python/.vscodeignore @@ -1,8 +1,3 @@ test/** -src/** -out/** -tsconfig.json -extension.webpack.config.js -extension-browser.webpack.config.js cgmanifest.json .vscode diff --git a/extensions/python/language-configuration.json b/extensions/python/language-configuration.json index 4717d09750..8e3f541412 100644 --- a/extensions/python/language-configuration.json +++ b/extensions/python/language-configuration.json @@ -46,5 +46,11 @@ "start": "^\\s*#\\s*region\\b", "end": "^\\s*#\\s*endregion\\b" } - } + }, + "onEnterRules": [ + { + "beforeText": "^\\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\\s*$", + "action": { "indent": "indent" } + } + ] } diff --git a/extensions/python/package.json b/extensions/python/package.json index f39c3d62b2..7413ef788b 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -1,36 +1,62 @@ { - "name": "python", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "activationEvents": ["onLanguage:python"], - "main": "./out/pythonMain", - "browser": "./dist/browser/pythonMain", - "extensionKind": [ "ui", "workspace", "web" ], - "contributes": { - "languages": [{ - "id": "python", - "extensions": [ ".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi", ".pyi", ".ipy"], - "aliases": [ "Python", "py" ], - "filenames": [ "Snakefile", "SConstruct", "SConscript" ], - "firstLine": "^#!\\s*/?.*\\bpython[0-9.-]*\\b", - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "python", - "scopeName": "source.python", - "path": "./syntaxes/MagicPython.tmLanguage.json" - },{ - "scopeName": "source.regexp.python", - "path": "./syntaxes/MagicRegExp.tmLanguage.json" - }] - }, - "scripts": { - "compile": "gulp compile-extension:python", - "watch": "gulp watch-extension:python", - "update-grammar": "node ../../build/npm/update-grammar.js MagicStack/MagicPython grammars/MagicPython.tmLanguage ./syntaxes/MagicPython.tmLanguage.json grammars/MagicRegExp.tmLanguage ./syntaxes/MagicRegExp.tmLanguage.json" - } + "name": "python", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "extensionKind": [ + "ui", + "workspace", + "web" + ], + "contributes": { + "languages": [ + { + "id": "python", + "extensions": [ + ".py", + ".rpy", + ".pyw", + ".cpy", + ".gyp", + ".gypi", + ".pyi", + ".ipy" + ], + "aliases": [ + "Python", + "py" + ], + "filenames": [ + "Snakefile", + "SConstruct", + "SConscript" + ], + "firstLine": "^#!\\s*/?.*\\bpython[0-9.-]*\\b", + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "python", + "scopeName": "source.python", + "path": "./syntaxes/MagicPython.tmLanguage.json" + }, + { + "scopeName": "source.regexp.python", + "path": "./syntaxes/MagicRegExp.tmLanguage.json" + } + ] + }, + "scripts": { + "update-grammar": "node ../node_modules/.bin/vscode-grammar-updater MagicStack/MagicPython grammars/MagicPython.tmLanguage ./syntaxes/MagicPython.tmLanguage.json grammars/MagicRegExp.tmLanguage ./syntaxes/MagicRegExp.tmLanguage.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/python/package.nls.json b/extensions/python/package.nls.json index 9bdaf5a3d6..bb1ed00954 100644 --- a/extensions/python/package.nls.json +++ b/extensions/python/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "Python Language Basics", "description": "Provides syntax highlighting, bracket matching and folding in Python files." -} \ No newline at end of file +} diff --git a/extensions/python/yarn.lock b/extensions/python/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/python/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/r/package.json b/extensions/r/package.json index 63ff6a6dcb..114c2122ce 100644 --- a/extensions/r/package.json +++ b/extensions/r/package.json @@ -1,25 +1,43 @@ { - "name": "r", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js Ikuyadeu/vscode-R syntax/r.json ./syntaxes/r.tmLanguage.json" - }, - "contributes": { - "languages": [{ - "id": "r", - "extensions": [ ".r", ".rhistory", ".rprofile", ".rt" ], - "aliases": [ "R", "r" ], - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "r", - "scopeName": "source.r", - "path": "./syntaxes/r.tmLanguage.json" - }] - } + "name": "r", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "scripts": { + "update-grammar": "node ../node_modules/.bin/vscode-grammar-updater Ikuyadeu/vscode-R syntax/r.json ./syntaxes/r.tmLanguage.json" + }, + "contributes": { + "languages": [ + { + "id": "r", + "extensions": [ + ".r", + ".rhistory", + ".rprofile", + ".rt" + ], + "aliases": [ + "R", + "r" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "r", + "scopeName": "source.r", + "path": "./syntaxes/r.tmLanguage.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/r/package.nls.json b/extensions/r/package.nls.json index e55a5012fd..c8d9c6c95c 100644 --- a/extensions/r/package.nls.json +++ b/extensions/r/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "R Language Basics", "description": "Provides syntax highlighting and bracket matching in R files." -} \ No newline at end of file +} diff --git a/extensions/r/yarn.lock b/extensions/r/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/r/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/resource-deployment/src/test/stubs.ts b/extensions/resource-deployment/src/test/stubs.ts index b7e586af87..803678a5e0 100644 --- a/extensions/resource-deployment/src/test/stubs.ts +++ b/extensions/resource-deployment/src/test/stubs.ts @@ -10,6 +10,10 @@ import { Readable } from 'stream'; export class TestChildProcessPromise implements cp.ChildProcessPromise { private _promise: Promise; private _event: events.EventEmitter = new events.EventEmitter(); + readonly exitCode: number | null = null; + readonly signalCode: number | null = null; + readonly spawnargs: string[] = []; + readonly spawnfile: string = ''; constructor() { this._promise = new Promise((resolve, reject) => { @@ -37,7 +41,7 @@ export class TestChildProcessPromise implements cp.ChildProcessPromise { killed: boolean = false; pid: number = -1; connected: boolean = false; - kill(signal?: number | 'SIGABRT' | 'SIGALRM' | 'SIGBUS' | 'SIGCHLD' | 'SIGCONT' | 'SIGFPE' | 'SIGHUP' | 'SIGILL' | 'SIGINT' | 'SIGIO' | 'SIGIOT' | 'SIGKILL' | 'SIGPIPE' | 'SIGPOLL' | 'SIGPROF' | 'SIGPWR' | 'SIGQUIT' | 'SIGSEGV' | 'SIGSTKFLT' | 'SIGSTOP' | 'SIGSYS' | 'SIGTERM' | 'SIGTRAP' | 'SIGTSTP' | 'SIGTTIN' | 'SIGTTOU' | 'SIGUNUSED' | 'SIGURG' | 'SIGUSR1' | 'SIGUSR2' | 'SIGVTALRM' | 'SIGWINCH' | 'SIGXCPU' | 'SIGXFSZ' | 'SIGBREAK' | 'SIGLOST' | 'SIGINFO'): void { + kill(signal?: NodeJS.Signals | number): boolean { throw new Error('Method not implemented.'); } diff --git a/extensions/schema-compare/src/test/testContext.ts b/extensions/schema-compare/src/test/testContext.ts index 12e4cf7455..09cca7060b 100644 --- a/extensions/schema-compare/src/test/testContext.ts +++ b/extensions/schema-compare/src/test/testContext.ts @@ -40,7 +40,8 @@ export function createContext(): TestContext { extensionMode: undefined as any, globalStorageUri: undefined, logUri: undefined, - storageUri: undefined + storageUri: undefined, + secrets: undefined }, viewContext: viewContext }; diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index 9635a6c51d..11b2a1eb91 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -15,7 +15,7 @@ "main": "./out/extension.js", "browser": "./dist/extension.js", "activationEvents": [ - "*" + "onLanguage:search-result" ], "scripts": { "generate-grammar": "node ./syntaxes/generateTMLanguage.js", @@ -45,5 +45,9 @@ "path": "./syntaxes/searchResult.tmLanguage.json" } ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" } } diff --git a/extensions/search-result/src/extension.ts b/extensions/search-result/src/extension.ts index c71c187a9b..97538c6adf 100644 --- a/extensions/search-result/src/extension.ts +++ b/extensions/search-result/src/extension.ts @@ -128,9 +128,19 @@ export function activate(context: vscode.ExtensionContext) { function relativePathToUri(path: string, resultsUri: vscode.Uri): vscode.Uri | undefined { - if (pathUtils.isAbsolute(path)) { return vscode.Uri.file(path); } + + const userDataPrefix = 'vscode-userdata:'; + if (path.startsWith(userDataPrefix)) { + return vscode.Uri.file(path.slice(userDataPrefix.length)).with({ scheme: 'vscode-userdata' }); + } + + if (pathUtils.isAbsolute(path)) { + return vscode.Uri.file(path); + } + if (path.indexOf('~/') === 0) { - return vscode.Uri.file(pathUtils.join(process.env.HOME!, path.slice(2))); + const homePath = process.env.HOME || process.env.HOMEPATH || ''; + return vscode.Uri.file(pathUtils.join(homePath, path.slice(2))); } const uriFromFolderWithPath = (folder: vscode.WorkspaceFolder, path: string): vscode.Uri => @@ -213,9 +223,18 @@ function parseSearchResults(document: vscode.TextDocument, token?: vscode.Cancel const metadataOffset = (indentation + _lineNumber + seperator).length; const targetRange = new vscode.Range(Math.max(lineNumber - 3, 0), 0, lineNumber + 3, line.length); + let locations: Required[] = []; + + // Allow line number, indentation, etc to take you to definition as well. + locations.push({ + targetRange, + targetSelectionRange: new vscode.Range(lineNumber, 0, lineNumber, 1), + targetUri: currentTarget, + originSelectionRange: new vscode.Range(i, 0, i, resultStart), + }); + let lastEnd = resultStart; let offset = 0; - let locations: Required[] = []; ELISION_REGEX.lastIndex = resultStart; for (let match: RegExpExecArray | null; (match = ELISION_REGEX.exec(line));) { locations.push({ diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index b030bd3994..f081480e14 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -70,10 +70,11 @@ function withNodeDefaults(/**@type WebpackConfig*/extConfig) { // yes, really source maps devtool: 'source-map', plugins: [ - // @ts-expect-error - new CopyWebpackPlugin([ - { from: 'src', to: '.', ignore: ['**/test/**', '*.ts'] } - ]), + new CopyWebpackPlugin({ + patterns: [ + { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } + ] + }), new NLSBundlePlugin(id) ], }; @@ -127,10 +128,11 @@ function withBrowserDefaults(/**@type WebpackConfig*/extConfig) { // yes, really source maps devtool: 'source-map', plugins: [ - // @ts-expect-error - new CopyWebpackPlugin([ - { from: 'src', to: '.', ignore: ['**/test/**', '*.ts'] } - ]), + new CopyWebpackPlugin({ + patterns: [ + { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } + ] + }), new DefinePlugin({ WEBWORKER: JSON.stringify(true) }) ] }; diff --git a/extensions/simple-browser/.vscodeignore b/extensions/simple-browser/.vscodeignore new file mode 100644 index 0000000000..9f1e062077 --- /dev/null +++ b/extensions/simple-browser/.vscodeignore @@ -0,0 +1,12 @@ +test/** +test-workspace/** +src/** +tsconfig.json +out/test/** +out/** +extension.webpack.config.js +extension-browser.webpack.config.js +cgmanifest.json +yarn.lock +preview-src/** +webpack.config.js diff --git a/extensions/simple-browser/README.md b/extensions/simple-browser/README.md new file mode 100644 index 0000000000..5cc65e7b19 --- /dev/null +++ b/extensions/simple-browser/README.md @@ -0,0 +1,3 @@ +# Simple Browser files + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. diff --git a/extensions/python/extension-browser.webpack.config.js b/extensions/simple-browser/extension-browser.webpack.config.js similarity index 87% rename from extensions/python/extension-browser.webpack.config.js rename to extensions/simple-browser/extension-browser.webpack.config.js index 785672b6c5..cc74eb2a15 100644 --- a/extensions/python/extension-browser.webpack.config.js +++ b/extensions/simple-browser/extension-browser.webpack.config.js @@ -12,9 +12,6 @@ const withBrowserDefaults = require('../shared.webpack.config').browser; module.exports = withBrowserDefaults({ context: __dirname, entry: { - extension: './src/pythonMain.ts' - }, - output: { - filename: 'pythonMain.js' + extension: './src/extension.ts' } }); diff --git a/extensions/python/extension.webpack.config.js b/extensions/simple-browser/extension.webpack.config.js similarity index 85% rename from extensions/python/extension.webpack.config.js rename to extensions/simple-browser/extension.webpack.config.js index 183f7f9a22..f35561d9f2 100644 --- a/extensions/python/extension.webpack.config.js +++ b/extensions/simple-browser/extension.webpack.config.js @@ -11,7 +11,10 @@ const withDefaults = require('../shared.webpack.config'); module.exports = withDefaults({ context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, entry: { - pythonMain: './src/pythonMain.ts' + extension: './src/extension.ts', } }); diff --git a/extensions/simple-browser/media/index.js b/extensions/simple-browser/media/index.js new file mode 100644 index 0000000000..b7ea59b1ba --- /dev/null +++ b/extensions/simple-browser/media/index.js @@ -0,0 +1,210 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "./preview-src/index.ts"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "./preview-src/events.ts": +/*!*******************************!*\ + !*** ./preview-src/events.ts ***! + \*******************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.onceDocumentLoaded = void 0; +function onceDocumentLoaded(f) { + if (document.readyState === 'loading' || document.readyState === 'uninitialized') { + document.addEventListener('DOMContentLoaded', f); + } + else { + f(); + } +} +exports.onceDocumentLoaded = onceDocumentLoaded; + + +/***/ }), + +/***/ "./preview-src/index.ts": +/*!******************************!*\ + !*** ./preview-src/index.ts ***! + \******************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const events_1 = __webpack_require__(/*! ./events */ "./preview-src/events.ts"); +const vscode = acquireVsCodeApi(); +function getSettings() { + const element = document.getElementById('simple-browser-settings'); + if (element) { + const data = element.getAttribute('data-settings'); + if (data) { + return JSON.parse(data); + } + } + throw new Error(`Could not load settings`); +} +const settings = getSettings(); +const iframe = document.querySelector('iframe'); +const header = document.querySelector('.header'); +const input = header.querySelector('.url-input'); +const forwardButton = header.querySelector('.forward-button'); +const backButton = header.querySelector('.back-button'); +const reloadButton = header.querySelector('.reload-button'); +const openExternalButton = header.querySelector('.open-external-button'); +window.addEventListener('message', e => { + switch (e.data.type) { + case 'focus': + { + iframe.focus(); + break; + } + case 'didChangeFocusLockIndicatorEnabled': + { + toggleFocusLockIndicatorEnabled(e.data.enabled); + break; + } + } +}); +events_1.onceDocumentLoaded(() => { + setInterval(() => { + var _a; + const iframeFocused = ((_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) === 'IFRAME'; + document.body.classList.toggle('iframe-focused', iframeFocused); + }, 50); + iframe.addEventListener('load', () => { + // Noop + }); + input.addEventListener('change', e => { + const url = e.target.value; + navigateTo(url); + }); + forwardButton.addEventListener('click', () => { + history.forward(); + }); + backButton.addEventListener('click', () => { + history.back(); + }); + openExternalButton.addEventListener('click', () => { + vscode.postMessage({ + type: 'openExternal', + url: input.value + }); + }); + reloadButton.addEventListener('click', () => { + // This does not seem to trigger what we want + // history.go(0); + // This incorrectly adds entries to the history but does reload + iframe.src = input.value; + }); + navigateTo(settings.url); + toggleFocusLockIndicatorEnabled(settings.focusLockIndicatorEnabled); + function navigateTo(url) { + iframe.src = url; + } +}); +function toggleFocusLockIndicatorEnabled(enabled) { + document.body.classList.toggle('enable-focus-lock-indicator', enabled); +} + + +/***/ }) + +/******/ }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/extensions/simple-browser/media/index.js.map b/extensions/simple-browser/media/index.js.map new file mode 100644 index 0000000000..d6128aae7e --- /dev/null +++ b/extensions/simple-browser/media/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./preview-src/events.ts","webpack:///./preview-src/index.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;AClFa;AACb;AACA;AACA;AACA;AACA,8CAA8C,cAAc;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;ACfa;AACb;AACA;AACA;AACA;AACA,8CAA8C,cAAc;AAC5D,iBAAiB,mBAAO,CAAC,yCAAU;AACnC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA,SAAS;AACT,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,CAAC;AACD;AACA;AACA","file":"index.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./preview-src/index.ts\");\n","\"use strict\";\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.onceDocumentLoaded = void 0;\nfunction onceDocumentLoaded(f) {\n if (document.readyState === 'loading' || document.readyState === 'uninitialized') {\n document.addEventListener('DOMContentLoaded', f);\n }\n else {\n f();\n }\n}\nexports.onceDocumentLoaded = onceDocumentLoaded;\n","\"use strict\";\n/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst events_1 = require(\"./events\");\nconst vscode = acquireVsCodeApi();\nfunction getSettings() {\n const element = document.getElementById('simple-browser-settings');\n if (element) {\n const data = element.getAttribute('data-settings');\n if (data) {\n return JSON.parse(data);\n }\n }\n throw new Error(`Could not load settings`);\n}\nconst settings = getSettings();\nconst iframe = document.querySelector('iframe');\nconst header = document.querySelector('.header');\nconst input = header.querySelector('.url-input');\nconst forwardButton = header.querySelector('.forward-button');\nconst backButton = header.querySelector('.back-button');\nconst reloadButton = header.querySelector('.reload-button');\nconst openExternalButton = header.querySelector('.open-external-button');\nwindow.addEventListener('message', e => {\n switch (e.data.type) {\n case 'focus':\n {\n iframe.focus();\n break;\n }\n case 'didChangeFocusLockIndicatorEnabled':\n {\n toggleFocusLockIndicatorEnabled(e.data.enabled);\n break;\n }\n }\n});\nevents_1.onceDocumentLoaded(() => {\n setInterval(() => {\n var _a;\n const iframeFocused = ((_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) === 'IFRAME';\n document.body.classList.toggle('iframe-focused', iframeFocused);\n }, 50);\n iframe.addEventListener('load', () => {\n // Noop\n });\n input.addEventListener('change', e => {\n const url = e.target.value;\n navigateTo(url);\n });\n forwardButton.addEventListener('click', () => {\n history.forward();\n });\n backButton.addEventListener('click', () => {\n history.back();\n });\n openExternalButton.addEventListener('click', () => {\n vscode.postMessage({\n type: 'openExternal',\n url: input.value\n });\n });\n reloadButton.addEventListener('click', () => {\n // This does not seem to trigger what we want\n // history.go(0);\n // This incorrectly adds entries to the history but does reload\n iframe.src = input.value;\n });\n navigateTo(settings.url);\n toggleFocusLockIndicatorEnabled(settings.focusLockIndicatorEnabled);\n function navigateTo(url) {\n iframe.src = url;\n }\n});\nfunction toggleFocusLockIndicatorEnabled(enabled) {\n document.body.classList.toggle('enable-focus-lock-indicator', enabled);\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/extensions/simple-browser/media/main.css b/extensions/simple-browser/media/main.css new file mode 100644 index 0000000000..6f2e9b071b --- /dev/null +++ b/extensions/simple-browser/media/main.css @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +:root { + --container-paddding: 20px; + --input-padding-vertical: 2px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +html, body { + height: 100%; + min-height: 100%; + padding: 0; + margin: 0; +} + +body { + display: grid; + grid-template-rows: auto 1fr; +} + +input:not([type='checkbox']), +textarea { + display: block; + width: 100%; + border: none; + font-family: var(--vscode-font-family); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + outline-color: var(--vscode-input-border); + background-color: var(--vscode-input-background); +} + +input::placeholder, +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +button { + border: none; + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + text-align: center; + outline: 1px solid transparent; + outline-offset: 2px !important; + color: var(--vscode-icon-foreground); + background: none; +} + +button:hover:not(:disabled) { + cursor: pointer; + background: var(--vscode-button-hoverBackground); +} + +button:disabled { + opacity: 0.5; + background: var(--vscode-button-background); +} + +button:focus { + outline-color: var(--vscode-focusBorder); +} + +.header { + display: flex; + margin: 0.4em 1em; +} + +.url-input { + flex: 1; +} + +.controls { + display: flex; +} + +.controls button { + display: flex; + margin-right: 0.3em; +} + +.content { + width: 100%; + height: 100%; + display: flex; + justify-content: center; +} + +iframe { + width: 100%; + height: 100%; + border: none; +} + +.iframe-focused-alert { + display: none; + position: absolute; + bottom: 1em; + background: var(--vscode-editorWidget-background); + color: var(--vscode-editorWidget-foreground); + padding: 0.2em 0.2em; + border-radius: 4px; + + font-size: 8px; + font-family: monospace; + user-select: none; + pointer-events: none; +} + +.iframe-focused.enable-focus-lock-indicator .iframe-focused-alert { + display: block; +} diff --git a/extensions/simple-browser/media/preview-dark.svg b/extensions/simple-browser/media/preview-dark.svg new file mode 100644 index 0000000000..ec71ea8114 --- /dev/null +++ b/extensions/simple-browser/media/preview-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/simple-browser/media/preview-light.svg b/extensions/simple-browser/media/preview-light.svg new file mode 100644 index 0000000000..4a6b85b583 --- /dev/null +++ b/extensions/simple-browser/media/preview-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json new file mode 100644 index 0000000000..a9489bc24c --- /dev/null +++ b/extensions/simple-browser/package.json @@ -0,0 +1,73 @@ +{ + "name": "simple-browser", + "displayName": "%displayName%", + "description": "%description%", + "enableProposedApi": true, + "version": "1.0.0", + "icon": "icon.png", + "publisher": "vscode", + "license": "MIT", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "engines": { + "vscode": "^1.53.0" + }, + "main": "./out/extension", + "browser": "./dist/browser/extension", + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:simpleBrowser.show", + "onCommand:simpleBrowser.api.open", + "onOpenExternalUri:http", + "onOpenExternalUri:https" + ], + "contributes": { + "commands": [ + { + "command": "simpleBrowser.show", + "title": "Show", + "category": "Simple Browser" + } + ], + "configuration": [ + { + "title": "Simple Browser", + "properties": { + "simpleBrowser.focusLockIndicator.enabled": { + "type": "boolean", + "default": true, + "title": "Focus Lock Indicator Enabled", + "description": "%configuration.focusLockIndicator.enabled.description%" + } + } + } + ] + }, + "scripts": { + "compile": "gulp compile-extension:markdown-language-features && npm run build-preview", + "watch": "npm run build-preview && gulp watch-extension:markdown-language-features", + "vscode:prepublish": "npm run build-ext && npm run build-preview", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json", + "build-preview": "webpack --mode development", + "build-preview-production": "webpack --mode production", + "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", + "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" + }, + "dependencies": { + "vscode-codicons": "^0.0.12", + "vscode-extension-telemetry": "0.1.1", + "vscode-nls": "^4.0.0" + }, + "devDependencies": { + "@types/node": "^12.11.7", + "ts-loader": "^6.2.1", + "typescript": "^3.7.3", + "webpack": "^4.41.2", + "webpack-cli": "^3.3.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } +} diff --git a/extensions/simple-browser/package.nls.json b/extensions/simple-browser/package.nls.json new file mode 100644 index 0000000000..496dc28dfd --- /dev/null +++ b/extensions/simple-browser/package.nls.json @@ -0,0 +1,5 @@ +{ + "displayName": "Simple Browser", + "description": "A very basic built-in webview for displaying web content.", + "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser." +} diff --git a/extensions/simple-browser/preview-src/events.ts b/extensions/simple-browser/preview-src/events.ts new file mode 100644 index 0000000000..8cb41f6661 --- /dev/null +++ b/extensions/simple-browser/preview-src/events.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function onceDocumentLoaded(f: () => void) { + if (document.readyState === 'loading' || document.readyState as string === 'uninitialized') { + document.addEventListener('DOMContentLoaded', f); + } else { + f(); + } +} \ No newline at end of file diff --git a/extensions/simple-browser/preview-src/index.ts b/extensions/simple-browser/preview-src/index.ts new file mode 100644 index 0000000000..ae03af24ed --- /dev/null +++ b/extensions/simple-browser/preview-src/index.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onceDocumentLoaded } from './events'; + +declare let acquireVsCodeApi: any; +const vscode = acquireVsCodeApi(); + +function getSettings() { + const element = document.getElementById('simple-browser-settings'); + if (element) { + const data = element.getAttribute('data-settings'); + if (data) { + return JSON.parse(data); + } + } + + throw new Error(`Could not load settings`); +} + +const settings = getSettings(); + +const iframe = document.querySelector('iframe')!; +const header = document.querySelector('.header')!; +const input = header.querySelector('.url-input')!; +const forwardButton = header.querySelector('.forward-button')!; +const backButton = header.querySelector('.back-button')!; +const reloadButton = header.querySelector('.reload-button')!; +const openExternalButton = header.querySelector('.open-external-button')!; + +window.addEventListener('message', e => { + switch (e.data.type) { + case 'focus': + { + iframe.focus(); + break; + } + case 'didChangeFocusLockIndicatorEnabled': + { + toggleFocusLockIndicatorEnabled(e.data.enabled); + break; + } + } +}); + +onceDocumentLoaded(() => { + setInterval(() => { + const iframeFocused = document.activeElement?.tagName === 'IFRAME'; + document.body.classList.toggle('iframe-focused', iframeFocused); + }, 50); + + iframe.addEventListener('load', () => { + // Noop + }); + + input.addEventListener('change', e => { + const url = (e.target as HTMLInputElement).value; + navigateTo(url); + }); + + forwardButton.addEventListener('click', () => { + history.forward(); + }); + + backButton.addEventListener('click', () => { + history.back(); + }); + + openExternalButton.addEventListener('click', () => { + vscode.postMessage({ + type: 'openExternal', + url: input.value + }); + }); + + reloadButton.addEventListener('click', () => { + // This does not seem to trigger what we want + // history.go(0); + + // This incorrectly adds entries to the history but does reload + iframe.src = input.value; + }); + + navigateTo(settings.url); + toggleFocusLockIndicatorEnabled(settings.focusLockIndicatorEnabled); + + function navigateTo(url: string): void { + iframe.src = url; + } +}); + +function toggleFocusLockIndicatorEnabled(enabled: boolean) { + document.body.classList.toggle('enable-focus-lock-indicator', enabled); +} + diff --git a/extensions/simple-browser/preview-src/tsconfig.json b/extensions/simple-browser/preview-src/tsconfig.json new file mode 100644 index 0000000000..e19cd4a675 --- /dev/null +++ b/extensions/simple-browser/preview-src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../shared.tsconfig.json", + "compilerOptions": { + "outDir": "./dist/", + "jsx": "react", + "lib": [ + "es2018", + "DOM", + "DOM.Iterable" + ] + } +} diff --git a/extensions/simple-browser/src/dispose.ts b/extensions/simple-browser/src/dispose.ts new file mode 100644 index 0000000000..33c2a708d8 --- /dev/null +++ b/extensions/simple-browser/src/dispose.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function disposeAll(disposables: vscode.Disposable[]) { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} diff --git a/extensions/simple-browser/src/extension.ts b/extensions/simple-browser/src/extension.ts new file mode 100644 index 0000000000..849a471842 --- /dev/null +++ b/extensions/simple-browser/src/extension.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { SimpleBrowserManager } from './simpleBrowserManager'; + +declare const URL: typeof import('url').URL; + +const localize = nls.loadMessageBundle(); + +const openApiCommand = 'simpleBrowser.api.open'; +const showCommand = 'simpleBrowser.show'; + +const enabledHosts = new Set([ + 'localhost', + '127.0.0.1' +]); + +const openerId = 'simpleBrowser.open'; + +export function activate(context: vscode.ExtensionContext) { + + const manager = new SimpleBrowserManager(context.extensionUri); + context.subscriptions.push(manager); + + context.subscriptions.push(vscode.commands.registerCommand(showCommand, async (url?: string) => { + if (!url) { + url = await vscode.window.showInputBox({ + placeHolder: localize('simpleBrowser.show.placeholder', "https://example.com"), + prompt: localize('simpleBrowser.show.prompt', "Enter url to visit") + }); + } + + if (url) { + manager.show(url); + } + })); + + context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, (url: vscode.Uri, showOptions?: { + preserveFocus?: boolean, + viewColumn: vscode.ViewColumn, + }) => { + manager.show(url.toString(), showOptions); + })); + + context.subscriptions.push(vscode.window.registerExternalUriOpener(openerId, { + canOpenExternalUri(uri: vscode.Uri) { + const originalUri = new URL(uri.toString()); + if (enabledHosts.has(originalUri.hostname)) { + return isWeb() + ? vscode.ExternalUriOpenerPriority.Default + : vscode.ExternalUriOpenerPriority.Option; + } + + return vscode.ExternalUriOpenerPriority.None; + }, + openExternalUri(resolveUri: vscode.Uri) { + return manager.show(resolveUri.toString(), { + viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active + }); + } + }, { + schemes: ['http', 'https'], + label: localize('openTitle', "Open in simple browser"), + })); +} + +function isWeb(): boolean { + // @ts-expect-error + return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; +} diff --git a/extensions/simple-browser/src/simpleBrowserManager.ts b/extensions/simple-browser/src/simpleBrowserManager.ts new file mode 100644 index 0000000000..240e3acc45 --- /dev/null +++ b/extensions/simple-browser/src/simpleBrowserManager.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ShowOptions, SimpleBrowserView } from './simpleBrowserView'; + +export class SimpleBrowserManager { + + private _activeView?: SimpleBrowserView; + + constructor( + private readonly extensionUri: vscode.Uri, + ) { } + + dispose() { + this._activeView?.dispose(); + this._activeView = undefined; + } + + public show(url: string, options?: ShowOptions): void { + if (this._activeView) { + this._activeView.show(url, options); + } else { + const view = new SimpleBrowserView(this.extensionUri, url, options); + view.onDispose(() => { + if (this._activeView === view) { + this._activeView = undefined; + } + }); + + this._activeView = view; + } + } +} + + diff --git a/extensions/simple-browser/src/simpleBrowserView.ts b/extensions/simple-browser/src/simpleBrowserView.ts new file mode 100644 index 0000000000..d845ad0797 --- /dev/null +++ b/extensions/simple-browser/src/simpleBrowserView.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { Disposable } from './dispose'; + +const localize = nls.loadMessageBundle(); + +export interface ShowOptions { + readonly preserveFocus?: boolean; + readonly viewColumn?: vscode.ViewColumn; +} + +export class SimpleBrowserView extends Disposable { + + public static readonly viewType = 'simpleBrowser.view'; + private static readonly title = localize('view.title', "Simple Browser"); + + private readonly _webviewPanel: vscode.WebviewPanel; + + private readonly _onDidDispose = this._register(new vscode.EventEmitter()); + public readonly onDispose = this._onDidDispose.event; + + constructor( + private readonly extensionUri: vscode.Uri, + url: string, + showOptions?: ShowOptions + ) { + super(); + + this._webviewPanel = this._register(vscode.window.createWebviewPanel(SimpleBrowserView.viewType, SimpleBrowserView.title, { + viewColumn: showOptions?.viewColumn ?? vscode.ViewColumn.Active, + preserveFocus: showOptions?.preserveFocus + }, { + enableScripts: true, + retainContextWhenHidden: true, + })); + + this._register(this._webviewPanel.webview.onDidReceiveMessage(e => { + switch (e.type) { + case 'openExternal': + try { + const url = vscode.Uri.parse(e.url); + vscode.env.openExternal(url); + } catch { + // Noop + } + break; + } + })); + + this._register(this._webviewPanel.onDidDispose(() => { + this.dispose(); + })); + + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('simpleBrowser.focusLockIndicator.enabled')) { + const configuration = vscode.workspace.getConfiguration('simpleBrowser'); + this._webviewPanel.webview.postMessage({ + type: 'didChangeFocusLockIndicatorEnabled', + focusLockEnabled: configuration.get('focusLockIndicator.enabled', true) + }); + } + })); + + this.show(url); + } + + public dispose() { + this._onDidDispose.fire(); + super.dispose(); + } + + public show(url: string, options?: ShowOptions) { + this._webviewPanel.webview.html = this.getHtml(url); + this._webviewPanel.reveal(options?.viewColumn, options?.preserveFocus); + } + + private getHtml(url: string) { + const configuration = vscode.workspace.getConfiguration('simpleBrowser'); + + const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); + + const mainJs = this.extensionResourceUrl('media', 'index.js'); + const mainCss = this.extensionResourceUrl('media', 'main.css'); + const codiconsUri = this.extensionResourceUrl('node_modules', 'vscode-codicons', 'dist', 'codicon.css'); + const codiconsFontUri = this.extensionResourceUrl('node_modules', 'vscode-codicons', 'dist', 'codicon.ttf'); + + return /* html */ ` + + + + + + + + + + + + +
+ + + + + +
+
+
${localize('view.iframe-focused', "Focus Lock")}
+ +
+ + + + `; + } + + private extensionResourceUrl(...parts: string[]): vscode.Uri { + return this._webviewPanel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, ...parts)); + } +} + +function escapeAttribute(value: string | vscode.Uri): string { + return value.toString().replace(/"/g, '"'); +} diff --git a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css b/extensions/simple-browser/src/typings/ref.d.ts similarity index 74% rename from src/vs/editor/contrib/documentSymbols/media/symbol-icons.css rename to extensions/simple-browser/src/typings/ref.d.ts index af39064849..c82a621bfa 100644 --- a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css +++ b/extensions/simple-browser/src/typings/ref.d.ts @@ -3,7 +3,5 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-icon-label.deprecated { - text-decoration: line-through; - opacity: 0.66; -} +/// +/// diff --git a/extensions/simple-browser/tsconfig.json b/extensions/simple-browser/tsconfig.json new file mode 100644 index 0000000000..d0797affba --- /dev/null +++ b/extensions/simple-browser/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../shared.tsconfig.json", + "compilerOptions": { + "outDir": "./out", + "experimentalDecorators": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/python/src/pythonMain.ts b/extensions/simple-browser/webpack.config.js similarity index 50% rename from extensions/python/src/pythonMain.ts rename to extensions/simple-browser/webpack.config.js index 1e6622da92..2a98826ded 100644 --- a/extensions/python/src/pythonMain.ts +++ b/extensions/simple-browser/webpack.config.js @@ -2,16 +2,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +const path = require('path'); -import { ExtensionContext, languages, IndentAction } from 'vscode'; - -export function activate(_context: ExtensionContext): any { - languages.setLanguageConfiguration('python', { - onEnterRules: [ +module.exports = { + entry: { + index: './preview-src/index.ts', + }, + devtool: 'source-map', + module: { + rules: [ { - beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\s*$/, - action: { indentAction: IndentAction.Indent } + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ } ] - }); -} \ No newline at end of file + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'] + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'media') + } +}; diff --git a/extensions/simple-browser/yarn.lock b/extensions/simple-browser/yarn.lock new file mode 100644 index 0000000000..1daa22f579 --- /dev/null +++ b/extensions/simple-browser/yarn.lock @@ -0,0 +1,2732 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^12.11.7": + version "12.12.69" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.69.tgz#7cb6a3aa0d16664bf2dcd1450ccb8477464fbd79" + integrity sha512-2F2VQRSFmzqgUEXw75L51MgnnZqc6bKWVSUPfrDPzp6mzGGibeVwyQcpvZvBr5RnsoMRHmC8EcBQiobSeqeJxg== + +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn@^6.4.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.1.0, ajv@^6.10.2: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +applicationinsights@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" + integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg== + dependencies: + diagnostic-channel "0.2.0" + diagnostic-channel-publishers "0.2.1" + zone.js "0.7.6" + +aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" + integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" + integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + dependencies: + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.3" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +cacache@^12.0.2: + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^2.3.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chokidar@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" + integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.1.2" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +cyclist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" + integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= + +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + +diagnostic-channel-publishers@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" + integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM= + +diagnostic-channel@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" + integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc= + dependencies: + semver "^5.3.0" + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +elliptic@^6.5.3: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esrecurse@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +events@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +figgy-pudding@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" + integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +infer-owner@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.4, ini@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +interpret@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@^1.0.2, loader-utils@^1.2.3, loader-utils@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^0.5.1, mkdirp@^0.5.3: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +nan@^2.12.1: + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +neo-async@^2.5.0, neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parallel-transform@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" + integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== + dependencies: + cyclist "^1.0.1" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0, parse-asn1@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +pbkdf2@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" + integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== + dependencies: + picomatch "^2.2.1" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rimraf@^2.5.4, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= + dependencies: + aproba "^1.1.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +safer-buffer@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +ssri@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + dependencies: + figgy-pudding "^3.5.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +tapable@^1.0.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^4.0.0" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser@^4.1.2: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +timers-browserify@^2.0.4: + version "2.0.11" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" + integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +ts-loader@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.2.tgz#dffa3879b01a1a1e0a4b85e2b8421dc0dfff1c58" + integrity sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@^3.7.3: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +uri-js@^4.2.2: + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +vscode-codicons@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/vscode-codicons/-/vscode-codicons-0.0.12.tgz#94b867156f3e3a6a4cf7661e9bf2bd0060042390" + integrity sha512-CyGFDf3OmQhtBql/BFheUAlvEZ1MKAgvyCrGktGXEsqYvK5m9456kckDTNvnEAGG8gYjE2ds5O1cle82sMgmAg== + +vscode-extension-telemetry@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b" + integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA== + dependencies: + applicationinsights "1.0.8" + +vscode-nls@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" + integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== + +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== + dependencies: + graceful-fs "^4.1.2" + neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.0" + +webpack-cli@^3.3.0: + version "3.3.12" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== + dependencies: + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" + +webpack-sources@^1.4.0, webpack-sources@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^4.41.2: + version "4.44.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.14, which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +zone.js@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" + integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk= diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index 90c0c3b4e4..4c5847262a 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -143,7 +143,8 @@ export function createContext(): TestContext { extensionMode: undefined as any, globalStorageUri: vscode.Uri.parse('test://'), logUri: vscode.Uri.parse('test://'), - storageUri: vscode.Uri.parse('test://') + storageUri: vscode.Uri.parse('test://'), + secrets: undefined as any }, dacFxService: TypeMoq.Mock.ofType(MockDacFxService) }; diff --git a/extensions/sql/build/update-grammar.js b/extensions/sql/build/update-grammar.js index ca8f4be3d2..81445bbffc 100644 --- a/extensions/sql/build/update-grammar.js +++ b/extensions/sql/build/update-grammar.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -var updateGrammar = require('../../../build/npm/update-grammar'); +var updateGrammar = require('vscode-grammar-updater'); updateGrammar.update('microsoft/vscode-mssql', 'syntaxes/SQL.plist', './syntaxes/sql.tmLanguage.json', undefined, 'main'); diff --git a/extensions/sql/package.json b/extensions/sql/package.json index 437d114c4e..ed0f77a4e3 100644 --- a/extensions/sql/package.json +++ b/extensions/sql/package.json @@ -1,36 +1,40 @@ { - "name": "sql", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { - "vscode": "*" - }, - "scripts": { - "update-grammar": "node ./build/update-grammar.js" - }, - "contributes": { - "languages": [ - { - "id": "sql", - "extensions": [ - ".sql", - ".dsql" - ], - "aliases": [ - "SQL" - ], - "configuration": "./language-configuration.json" - } - ], - "grammars": [ - { - "language": "sql", - "scopeName": "source.sql", - "path": "./syntaxes/sql.tmLanguage.json" - } - ] - } + "name": "sql", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "scripts": { + "update-grammar": "node ./build/update-grammar.js" + }, + "contributes": { + "languages": [ + { + "id": "sql", + "extensions": [ + ".sql", + ".dsql" + ], + "aliases": [ + "SQL" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "sql", + "scopeName": "source.sql", + "path": "./syntaxes/sql.tmLanguage.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/sql/package.nls.json b/extensions/sql/package.nls.json index 328fcb3e15..8ac71c71b5 100644 --- a/extensions/sql/package.nls.json +++ b/extensions/sql/package.nls.json @@ -1,4 +1,4 @@ { "displayName": "SQL Language Basics", "description": "Provides syntax highlighting and bracket matching in SQL files." -} \ No newline at end of file +} diff --git a/extensions/sql/yarn.lock b/extensions/sql/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/sql/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/testing-editor-contributions/.vscodeignore b/extensions/testing-editor-contributions/.vscodeignore new file mode 100644 index 0000000000..da3d276368 --- /dev/null +++ b/extensions/testing-editor-contributions/.vscodeignore @@ -0,0 +1,6 @@ +src/** +out/** +tsconfig.json +extension.webpack.config.js +extension-browser.webpack.config.js +yarn.lock diff --git a/extensions/testing-editor-contributions/README.md b/extensions/testing-editor-contributions/README.md new file mode 100644 index 0000000000..721f6c7c3e --- /dev/null +++ b/extensions/testing-editor-contributions/README.md @@ -0,0 +1,5 @@ +# Testing Editor Contributions + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +Provides the in-editor experience for tests and test results diff --git a/extensions/testing-editor-contributions/extension-browser.webpack.config.js b/extensions/testing-editor-contributions/extension-browser.webpack.config.js new file mode 100644 index 0000000000..e7253e1211 --- /dev/null +++ b/extensions/testing-editor-contributions/extension-browser.webpack.config.js @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withBrowserDefaults = require('../shared.webpack.config').browser; +const path = require('path'); + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + }, + output: { + filename: 'extension.js', + path: path.join(__dirname, 'dist') + } +}); diff --git a/extensions/testing-editor-contributions/extension.webpack.config.js b/extensions/testing-editor-contributions/extension.webpack.config.js new file mode 100644 index 0000000000..f35561d9f2 --- /dev/null +++ b/extensions/testing-editor-contributions/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + } +}); diff --git a/extensions/testing-editor-contributions/package.json b/extensions/testing-editor-contributions/package.json new file mode 100644 index 0000000000..1bb134f6f0 --- /dev/null +++ b/extensions/testing-editor-contributions/package.json @@ -0,0 +1,52 @@ +{ + "name": "testing-editor-contributions", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "enableProposedApi": true, + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.39.0" + }, + "categories": [ + "Other" + ], + "main": "./out/extension.js", + "browser": "./dist/extension.js", + "activationEvents": [ + "onStartupFinished" + ], + "dependencies": { + "vscode-nls": "^5.0.0" + }, + "contributes": { + "configuration": { + "title": "Testing", + "properties": { + "testing.enableCodeLens": { + "description": "%config.enableCodeLens%", + "type": "boolean", + "default": true + }, + "testing.enableProblemDiagnostics": { + "description": "%config.enableProblemDiagnostics%", + "type": "boolean", + "default": false + } + } + } + }, + "scripts": { + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:testing-editor-contributions ./tsconfig.json" + }, + "prettier": { + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } +} diff --git a/extensions/testing-editor-contributions/package.nls.json b/extensions/testing-editor-contributions/package.nls.json new file mode 100644 index 0000000000..83d8ef5716 --- /dev/null +++ b/extensions/testing-editor-contributions/package.nls.json @@ -0,0 +1,19 @@ +{ + "displayName": "Testing Editor Contributions", + "description": "Provides the in-editor experience for tests and test results.", + + "action.run": "Run Tests", + "action.debug": "Debug", + + "tooltip.run": "Run {0}", + "tooltip.debug": "Debug {0}", + "tooltip.runState": "{0}/{1} Tests Passed", + "tooltip.runStateWithDuration": "{0}/{1} Tests Passed in {2}", + + "state.failed": "Failed", + "state.passed": "Passed", + "state.passedWithDuration": "Passed in {0}", + + "config.enableCodeLens": "Whether code lens on test cases and suites should be visible", + "config.enableProblemDiagnostics": "Whether test failures should be reported in the 'problems' view and show as errors in the editor." +} diff --git a/extensions/testing-editor-contributions/src/extension.ts b/extensions/testing-editor-contributions/src/extension.ts new file mode 100644 index 0000000000..c7bdb02418 --- /dev/null +++ b/extensions/testing-editor-contributions/src/extension.ts @@ -0,0 +1,412 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +interface IDisposable { + dispose(): void; +} + +const enum Constants { + ConfigSection = 'testing', + EnableCodeLensConfig = 'enableCodeLens', + EnableDiagnosticsConfig = 'enableProblemDiagnostics', +} + +export function activate(context: vscode.ExtensionContext) { + const diagnostics = vscode.languages.createDiagnosticCollection(); + const services = new TestingEditorServices(diagnostics); + context.subscriptions.push( + services, + diagnostics, + vscode.languages.registerCodeLensProvider({ scheme: 'file' }, services), + ); +} + +class TestingConfig implements IDisposable { + private section = vscode.workspace.getConfiguration(Constants.ConfigSection); + private readonly changeEmitter = new vscode.EventEmitter(); + private readonly listener = vscode.workspace.onDidChangeConfiguration(evt => { + if (evt.affectsConfiguration(Constants.ConfigSection)) { + this.section = vscode.workspace.getConfiguration(Constants.ConfigSection); + this.changeEmitter.fire(); + } + }); + + public readonly onChange = this.changeEmitter.event; + + public get codeLens() { + return this.section.get(Constants.EnableCodeLensConfig, true); + } + + public get diagnostics() { + return this.section.get(Constants.EnableDiagnosticsConfig, false); + } + + public get isEnabled() { + return this.codeLens || this.diagnostics; + } + + public dispose() { + this.listener.dispose(); + } +} + +export class TestingEditorServices implements IDisposable, vscode.CodeLensProvider { + private readonly codeLensChangeEmitter = new vscode.EventEmitter(); + private readonly documents = new Map(); + private readonly config = new TestingConfig(); + private disposables: IDisposable[]; + private wasEnabled = this.config.isEnabled; + + /** + * @inheritdoc + */ + public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event; + + constructor(private readonly diagnostics: vscode.DiagnosticCollection) { + this.disposables = [ + new vscode.Disposable(() => this.expireAll()), + + this.config, + + vscode.window.onDidChangeVisibleTextEditors((editors) => { + if (!this.config.isEnabled) { + return; + } + + const expiredEditors = new Set(this.documents.keys()); + for (const editor of editors) { + const key = editor.document.uri.toString(); + this.ensure(key, editor.document); + expiredEditors.delete(key); + } + + for (const expired of expiredEditors) { + this.expire(expired); + } + }), + + vscode.workspace.onDidCloseTextDocument((document) => { + this.expire(document.uri.toString()); + }), + + this.config.onChange(() => { + if (!this.wasEnabled || this.config.isEnabled) { + this.attachToAllVisible(); + } else if (this.wasEnabled || !this.config.isEnabled) { + this.expireAll(); + } + + this.wasEnabled = this.config.isEnabled; + this.codeLensChangeEmitter.fire(); + }), + ]; + + if (this.config.isEnabled) { + this.attachToAllVisible(); + } + } + + /** + * @inheritdoc + */ + public provideCodeLenses(document: vscode.TextDocument) { + if (!this.config.codeLens) { + return []; + } + + return this.documents.get(document.uri.toString())?.provideCodeLenses() ?? []; + } + + /** + * Attach to all currently visible editors. + */ + private attachToAllVisible() { + for (const editor of vscode.window.visibleTextEditors) { + this.ensure(editor.document.uri.toString(), editor.document); + } + } + + /** + * Unattaches to all tests. + */ + private expireAll() { + for (const observer of this.documents.values()) { + observer.dispose(); + } + + this.documents.clear(); + } + + /** + * Subscribes to tests for the document URI. + */ + private ensure(key: string, document: vscode.TextDocument) { + const state = this.documents.get(key); + if (!state) { + const observer = new DocumentTestObserver(document, this.diagnostics, this.config); + this.documents.set(key, observer); + observer.onDidChangeCodeLenses(() => this.config.codeLens && this.codeLensChangeEmitter.fire()); + } + } + + /** + * Expires and removes the watcher for the document. + */ + private expire(key: string) { + const observer = this.documents.get(key); + if (!observer) { + return; + } + + observer.dispose(); + this.documents.delete(key); + } + + /** + * @override + */ + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} + +class DocumentTestObserver implements IDisposable { + private readonly codeLensChangeEmitter = new vscode.EventEmitter(); + private readonly observer = vscode.test.createDocumentTestObserver(this.document); + private readonly disposables: IDisposable[]; + public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event; + private didHaveDiagnostics = this.config.diagnostics; + + constructor( + private readonly document: vscode.TextDocument, + private readonly diagnostics: vscode.DiagnosticCollection, + private readonly config: TestingConfig, + ) { + this.disposables = [ + this.observer, + this.codeLensChangeEmitter, + + config.onChange(() => { + if (this.didHaveDiagnostics && !config.diagnostics) { + this.diagnostics.set(document.uri, []); + } else if (!this.didHaveDiagnostics && config.diagnostics) { + this.updateDiagnostics(); + } + + this.didHaveDiagnostics = config.diagnostics; + }), + + this.observer.onDidChangeTest(() => { + this.updateDiagnostics(); + this.codeLensChangeEmitter.fire(); + }), + ]; + + } + + private updateDiagnostics() { + if (!this.config.diagnostics) { + return; + } + + const uriString = this.document.uri.toString(); + const diagnostics: vscode.Diagnostic[] = []; + for (const test of iterateOverTests(this.observer.tests)) { + for (const message of test.state.messages) { + if (message.location?.uri.toString() === uriString) { + diagnostics.push({ + range: message.location.range, + message: message.message.toString(), + severity: testToDiagnosticSeverity(message.severity), + }); + } + } + } + + this.diagnostics.set(this.document.uri, diagnostics); + } + + public provideCodeLenses(): vscode.CodeLens[] { + const lenses: vscode.CodeLens[] = []; + + for (const test of iterateOverTests(this.observer.tests)) { + const { debuggable = false, runnable = true } = test; + if (!test.location || !(debuggable || runnable)) { + continue; + } + + const summary = summarize(test); + + lenses.push({ + isResolved: true, + range: test.location.range, + command: { + title: `$(${testStateToIcon[summary.computedState]}) ${getLabelFor(test, summary)}`, + command: 'vscode.runTests', + arguments: [[test]], + tooltip: localize('tooltip.debug', 'Debug {0}', test.label), + }, + }); + + if (debuggable) { + lenses.push({ + isResolved: true, + range: test.location.range, + command: { + title: localize('action.debug', 'Debug'), + command: 'vscode.debugTests', + arguments: [[test]], + tooltip: localize('tooltip.debug', 'Debug {0}', test.label), + }, + }); + } + } + + return lenses; + } + + /** + * @override + */ + public dispose() { + this.diagnostics.set(this.document.uri, []); + this.disposables.forEach(d => d.dispose()); + } +} + +function getLabelFor(test: vscode.TestItem, summary: ITestSummary) { + if (summary.duration !== undefined) { + return localize( + 'tooltip.runStateWithDuration', + '{0}/{1} Tests Passed in {2}', + summary.passed, + summary.passed + summary.failed, + formatDuration(summary.duration), + ); + } + + if (summary.passed > 0 || summary.failed > 0) { + return localize('tooltip.runState', '{0}/{1} Tests Passed', summary.passed, summary.failed); + } + + if (test.state.runState === vscode.TestRunState.Passed) { + return test.state.duration !== undefined + ? localize('state.passedWithDuration', 'Passed in {0}', formatDuration(test.state.duration)) + : localize('state.passed', 'Passed'); + } + + if (isFailedState(test.state.runState)) { + return localize('state.failed', 'Failed'); + } + + return localize('action.run', 'Run Tests'); +} + +function formatDuration(duration: number) { + if (duration < 1_000) { + return `${Math.round(duration)}ms`; + } + + if (duration < 100_000) { + return `${(duration / 1000).toPrecision(3)}s`; + } + + return `${(duration / 1000 / 60).toPrecision(3)}m`; +} + +const statePriority: { [K in vscode.TestRunState]: number } = { + [vscode.TestRunState.Running]: 6, + [vscode.TestRunState.Queued]: 5, + [vscode.TestRunState.Errored]: 4, + [vscode.TestRunState.Failed]: 3, + [vscode.TestRunState.Passed]: 2, + [vscode.TestRunState.Skipped]: 1, + [vscode.TestRunState.Unset]: 0, +}; + +const maxPriority = (a: vscode.TestRunState, b: vscode.TestRunState) => + statePriority[a] > statePriority[b] ? a : b; + +const isFailedState = (s: vscode.TestRunState) => + s === vscode.TestRunState.Failed || s === vscode.TestRunState.Errored; + +interface ITestSummary { + passed: number; + failed: number; + duration: number | undefined; + computedState: vscode.TestRunState; +} + +function summarize(test: vscode.TestItem) { + let passed = 0; + let failed = 0; + let duration: number | undefined; + let computedState = test.state.runState; + + const queue = test.children ? [test.children] : []; + while (queue.length) { + for (const test of queue.pop()!) { + computedState = maxPriority(computedState, test.state.runState); + if (test.state.runState === vscode.TestRunState.Passed) { + passed++; + if (test.state.duration !== undefined) { + duration = test.state.duration + (duration ?? 0); + } + } else if (isFailedState(test.state.runState)) { + failed++; + if (test.state.duration !== undefined) { + duration = test.state.duration + (duration ?? 0); + } + } + + if (test.children) { + queue.push(test.children); + } + } + } + + return { passed, failed, duration, computedState }; +} + +function* iterateOverTests(tests: ReadonlyArray) { + const queue = [tests]; + while (queue.length) { + for (const test of queue.pop()!) { + yield test; + if (test.children) { + queue.push(test.children); + } + } + } +} + +const testStateToIcon: { [K in vscode.TestRunState]: string } = { + [vscode.TestRunState.Errored]: 'testing-error-icon', + [vscode.TestRunState.Failed]: 'testing-failed-icon', + [vscode.TestRunState.Passed]: 'testing-passed-icon', + [vscode.TestRunState.Queued]: 'testing-queued-icon', + [vscode.TestRunState.Skipped]: 'testing-skipped-icon', + [vscode.TestRunState.Unset]: 'beaker', + [vscode.TestRunState.Running]: 'loading~spin', +}; + +const testToDiagnosticSeverity = (severity: vscode.TestMessageSeverity | undefined) => { + switch (severity) { + case vscode.TestMessageSeverity.Hint: + return vscode.DiagnosticSeverity.Hint; + case vscode.TestMessageSeverity.Information: + return vscode.DiagnosticSeverity.Information; + case vscode.TestMessageSeverity.Warning: + return vscode.DiagnosticSeverity.Warning; + case vscode.TestMessageSeverity.Error: + default: + return vscode.DiagnosticSeverity.Error; + } +}; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-modifications.css b/extensions/testing-editor-contributions/src/typings/refs.d.ts similarity index 74% rename from src/vs/base/browser/ui/codicons/codicon/codicon-modifications.css rename to extensions/testing-editor-contributions/src/typings/refs.d.ts index 7245e86d58..c82a621bfa 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon-modifications.css +++ b/extensions/testing-editor-contributions/src/typings/refs.d.ts @@ -3,6 +3,5 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.codicon-wrench-subaction { - opacity: 0.5; -} +/// +/// diff --git a/extensions/python/tsconfig.json b/extensions/testing-editor-contributions/tsconfig.json similarity index 80% rename from extensions/python/tsconfig.json rename to extensions/testing-editor-contributions/tsconfig.json index 296ddb38fc..16ed233f6e 100644 --- a/extensions/python/tsconfig.json +++ b/extensions/testing-editor-contributions/tsconfig.json @@ -1,9 +1,9 @@ { "extends": "../shared.tsconfig.json", "compilerOptions": { - "outDir": "./out" + "outDir": "./out", }, "include": [ "src/**/*" ] -} \ No newline at end of file +} diff --git a/extensions/testing-editor-contributions/yarn.lock b/extensions/testing-editor-contributions/yarn.lock new file mode 100644 index 0000000000..9250709d1e --- /dev/null +++ b/extensions/testing-editor-contributions/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +vscode-nls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" + integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== diff --git a/extensions/theme-abyss/package.json b/extensions/theme-abyss/package.json index 9543824f33..57183bf3d5 100644 --- a/extensions/theme-abyss/package.json +++ b/extensions/theme-abyss/package.json @@ -1,19 +1,25 @@ { - "name": "theme-abyss", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "themes": [ - { - "id": "Abyss", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/abyss-color-theme.json" - } - ] - } + "name": "theme-abyss", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Abyss", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/abyss-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-abyss/yarn.lock b/extensions/theme-abyss/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-abyss/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-defaults/package.json b/extensions/theme-defaults/package.json index a9f87d81ee..d91ac173eb 100644 --- a/extensions/theme-defaults/package.json +++ b/extensions/theme-defaults/package.json @@ -22,5 +22,9 @@ "label": "Minimal (Azure Data Studio)", "path": "./fileicons/vs_minimal-icon-theme.json" } - ] + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/azuredatastudio.git" + } } \ No newline at end of file diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 3a5008aef7..061d360e3f 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -1,7 +1,25 @@ { "$schema": "vscode://schemas/color-theme", "name": "Dark (Visual Studio)", - "include": "./dark_defaults.json", + "colors": { + "editor.background": "#1E1E1E", + "editor.foreground": "#D4D4D4", + "editor.inactiveSelectionBackground": "#3A3D41", + "editorIndentGuide.background": "#404040", + "editorIndentGuide.activeBackground": "#707070", + "editor.selectionHighlightBackground": "#ADD6FF26", + "list.dropBackground": "#383B3D", + "activityBarBadge.background": "#007ACC", + "sideBarTitle.foreground": "#BBBBBB", + "input.placeholderForeground": "#A6A6A6", + "menu.background": "#252526", + "menu.foreground": "#CCCCCC", + "statusBarItem.remoteForeground": "#FFF", + "statusBarItem.remoteBackground": "#16825D", + "sideBarSectionHeader.background": "#0000", + "sideBarSectionHeader.border": "#ccc3", + "tab.lastPinnedBorder": "#ccc3" + }, "tokenColors": [ { "scope": [ @@ -362,6 +380,7 @@ } } ], + "semanticHighlighting": true, "semanticTokenColors": { "newOperator": "#d4d4d4", "stringLiteral": "#ce9178", diff --git a/extensions/theme-defaults/themes/hc_black.json b/extensions/theme-defaults/themes/hc_black.json index 436dfa5291..fcb350ac42 100644 --- a/extensions/theme-defaults/themes/hc_black.json +++ b/extensions/theme-defaults/themes/hc_black.json @@ -1,13 +1,335 @@ { "$schema": "vscode://schemas/color-theme", "name": "Dark High Contrast", - "include": "./hc_black_defaults.json", "colors": { + "editor.background": "#000000", + "editor.foreground": "#FFFFFF", + "editorIndentGuide.background": "#FFFFFF", + "editorIndentGuide.activeBackground": "#FFFFFF", + "sideBarTitle.foreground": "#FFFFFF", "selection.background": "#008000", "editor.selectionBackground": "#FFFFFF", "statusBarItem.remoteBackground": "#00000000" }, "tokenColors": [ + { + "scope": [ + "meta.embedded", + "source.groovy.embedded" + ], + "settings": { + "foreground": "#FFFFFF" + } + }, + { + "scope": "emphasis", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "strong", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#000080" + } + }, + { + "scope": "comment", + "settings": { + "foreground": "#7ca668" + } + }, + { + "scope": "constant.language", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": [ + "constant.numeric", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "constant.regexp", + "settings": { + "foreground": "#b46695" + } + }, + { + "scope": "constant.character", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "entity.name.tag", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "entity.name.tag.css", + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "entity.other.attribute-name", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": [ + "entity.other.attribute-name.class.css", + "entity.other.attribute-name.class.mixin.css", + "entity.other.attribute-name.id.css", + "entity.other.attribute-name.parent-selector.css", + "entity.other.attribute-name.pseudo-class.css", + "entity.other.attribute-name.pseudo-element.css", + "source.css.less entity.other.attribute-name.id", + "entity.other.attribute-name.scss" + ], + "settings": { + "foreground": "#d7ba7d" + } + }, + { + "scope": "invalid", + "settings": { + "foreground": "#f44747" + } + }, + { + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": "markup.heading", + "settings": { + "fontStyle": "bold", + "foreground": "#6796e6" + } + }, + { + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": "markup.inserted", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "markup.deleted", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "markup.changed", + "settings": { + "foreground": "#569cd6" + } + }, + { + "name": "brackets of XML/HTML tags", + "scope": [ + "punctuation.definition.tag" + ], + "settings": { + "foreground": "#808080" + } + }, + { + "scope": "meta.preprocessor", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "meta.preprocessor.string", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "meta.preprocessor.numeric", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "meta.structure.dictionary.key.python", + "settings": { + "foreground": "#9cdcfe" + } + }, + { + "scope": "storage", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "storage.type", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "storage.modifier", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "string", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.tag", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.value", + "settings": { + "foreground": "#ce9178" + } + }, + { + "scope": "string.regexp", + "settings": { + "foreground": "#d16969" + } + }, + { + "name": "String interpolation", + "scope": [ + "punctuation.definition.template-expression.begin", + "punctuation.definition.template-expression.end", + "punctuation.section.embedded" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "name": "Reset JavaScript string interpolation expression", + "scope": [ + "meta.template.expression" + ], + "settings": { + "foreground": "#ffffff" + } + }, + { + "scope": [ + "support.type.vendored.property-name", + "support.type.property-name", + "variable.css", + "variable.scss", + "variable.other.less", + "source.coffee.embedded" + ], + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": "keyword", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.control", + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.operator", + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": [ + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.cast", + "keyword.operator.sizeof", + "keyword.operator.logical.python" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": "keyword.other.unit", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "scope": "support.function.git-rebase", + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "scope": "constant.sha.git-rebase", + "settings": { + "foreground": "#b5cea8" + } + }, + { + "name": "coloring of the Java import and package identifiers", + "scope": [ + "storage.modifier.import.java", + "variable.language.wildcard.java", + "storage.modifier.package.java" + ], + "settings": { + "foreground": "#d4d4d4" + } + }, + { + "name": "coloring of the TS this", + "scope": "variable.language.this", + "settings": { + "foreground": "#569cd6" + } + }, { "name": "Function declarations", "scope": [ @@ -123,6 +445,7 @@ } } ], + "semanticHighlighting": true, "semanticTokenColors": { "newOperator": "#FFFFFF", "stringLiteral": "#ce9178", diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json deleted file mode 100644 index d0382cec29..0000000000 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ /dev/null @@ -1,340 +0,0 @@ -{ - "$schema": "vscode://schemas/color-theme", - "name": "High Contrast Default Colors", - "colors": { - "editor.background": "#000000", - "editor.foreground": "#FFFFFF", - "editorIndentGuide.background": "#FFFFFF", - "editorIndentGuide.activeBackground": "#FFFFFF", - "statusBarItem.remoteBackground": "#00000000", - "sideBarTitle.foreground": "#FFFFFF" - }, - "settings": [ - { - "settings": { - "foreground": "#FFFFFF", - "background": "#000000" - } - }, - { - "scope": [ - "meta.embedded", - "source.groovy.embedded" - ], - "settings": { - "foreground": "#FFFFFF", - "background": "#000000" - } - }, - { - "scope": "emphasis", - "settings": { - "fontStyle": "italic" - } - }, - { - "scope": "strong", - "settings": { - "fontStyle": "bold" - } - }, - { - "scope": "meta.diff.header", - "settings": { - "foreground": "#000080" - } - }, - { - "scope": "comment", - "settings": { - "foreground": "#7ca668" - } - }, - { - "scope": "constant.language", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": [ - "constant.numeric", - "constant.other.color.rgb-value", - "constant.other.rgb-value", - "support.constant.color" - ], - "settings": { - "foreground": "#b5cea8" - } - }, - { - "scope": "constant.regexp", - "settings": { - "foreground": "#b46695" - } - }, - { - "scope": "constant.character", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "entity.name.tag", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "entity.name.tag.css", - "settings": { - "foreground": "#d7ba7d" - } - }, - { - "scope": "entity.other.attribute-name", - "settings": { - "foreground": "#9cdcfe" - } - }, - { - "scope": [ - "entity.other.attribute-name.class.css", - "entity.other.attribute-name.class.mixin.css", - "entity.other.attribute-name.id.css", - "entity.other.attribute-name.parent-selector.css", - "entity.other.attribute-name.pseudo-class.css", - "entity.other.attribute-name.pseudo-element.css", - "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.scss" - ], - "settings": { - "foreground": "#d7ba7d" - } - }, - { - "scope": "invalid", - "settings": { - "foreground": "#f44747" - } - }, - { - "scope": "markup.underline", - "settings": { - "fontStyle": "underline" - } - }, - { - "scope": "markup.bold", - "settings": { - "fontStyle": "bold" - } - }, - { - "scope": "markup.heading", - "settings": { - "fontStyle": "bold", - "foreground": "#6796e6" - } - }, - { - "scope": "markup.italic", - "settings": { - "fontStyle": "italic" - } - }, - { - "scope": "markup.inserted", - "settings": { - "foreground": "#b5cea8" - } - }, - { - "scope": "markup.deleted", - "settings": { - "foreground": "#ce9178" - } - }, - { - "scope": "markup.changed", - "settings": { - "foreground": "#569cd6" - } - }, - { - "name": "brackets of XML/HTML tags", - "scope": [ - "punctuation.definition.tag" - ], - "settings": { - "foreground": "#808080" - } - }, - { - "scope": "meta.preprocessor", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "meta.preprocessor.string", - "settings": { - "foreground": "#ce9178" - } - }, - { - "scope": "meta.preprocessor.numeric", - "settings": { - "foreground": "#b5cea8" - } - }, - { - "scope": "meta.structure.dictionary.key.python", - "settings": { - "foreground": "#9cdcfe" - } - }, - { - "scope": "storage", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "storage.type", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "storage.modifier", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "string", - "settings": { - "foreground": "#ce9178" - } - }, - { - "scope": "string.tag", - "settings": { - "foreground": "#ce9178" - } - }, - { - "scope": "string.value", - "settings": { - "foreground": "#ce9178" - } - }, - { - "scope": "string.regexp", - "settings": { - "foreground": "#d16969" - } - }, - { - "name": "String interpolation", - "scope": [ - "punctuation.definition.template-expression.begin", - "punctuation.definition.template-expression.end", - "punctuation.section.embedded" - ], - "settings": { - "foreground": "#569cd6" - } - }, - { - "name": "Reset JavaScript string interpolation expression", - "scope": [ - "meta.template.expression" - ], - "settings": { - "foreground": "#ffffff" - } - }, - { - "scope": [ - "support.type.vendored.property-name", - "support.type.property-name", - "variable.css", - "variable.scss", - "variable.other.less", - "source.coffee.embedded" - ], - "settings": { - "foreground": "#d4d4d4" - } - }, - { - "scope": "keyword", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "keyword.control", - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "keyword.operator", - "settings": { - "foreground": "#d4d4d4" - } - }, - { - "scope": [ - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.cast", - "keyword.operator.sizeof", - "keyword.operator.logical.python" - ], - "settings": { - "foreground": "#569cd6" - } - }, - { - "scope": "keyword.other.unit", - "settings": { - "foreground": "#b5cea8" - } - }, - { - "scope": "support.function.git-rebase", - "settings": { - "foreground": "#d4d4d4" - } - }, - { - "scope": "constant.sha.git-rebase", - "settings": { - "foreground": "#b5cea8" - } - }, - { - "name": "coloring of the Java import and package identifiers", - "scope": [ - "storage.modifier.import.java", - "variable.language.wildcard.java", - "storage.modifier.package.java" - ], - "settings": { - "foreground": "#d4d4d4" - } - }, - { - "name": "coloring of the TS this", - "scope": "variable.language.this", - "settings": { - "foreground": "#569cd6" - } - } - ], - "semanticHighlighting": true -} diff --git a/extensions/theme-defaults/themes/light_defaults.json b/extensions/theme-defaults/themes/light_defaults.json deleted file mode 100644 index 05fce6cd1d..0000000000 --- a/extensions/theme-defaults/themes/light_defaults.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "vscode://schemas/color-theme", - "name": "Light Default Colors", - "colors": { - "editor.background": "#FFFFFF", - "editor.foreground": "#000000", - "editor.inactiveSelectionBackground": "#E5EBF1", - "editorIndentGuide.background": "#D3D3D3", - "editorIndentGuide.activeBackground": "#939393", - "editor.selectionHighlightBackground": "#ADD6FF80", - "editorSuggestWidget.background": "#F3F3F3", - "activityBarBadge.background": "#007ACC", - "sideBarTitle.foreground": "#6F6F6F", - "list.hoverBackground": "#E8E8E8", - "input.placeholderForeground": "#767676", - "searchEditor.textInputBorder": "#CECECE", - "settings.textInputBorder": "#CECECE", - "settings.numberInputBorder": "#CECECE", - "statusBarItem.remoteForeground": "#FFF", - "statusBarItem.remoteBackground": "#16825D", - "sideBarSectionHeader.background": "#0000", - "sideBarSectionHeader.border": "#61616130", - "tab.lastPinnedBorder": "#61616130", - "notebook.cellBorderColor": "#E8E8E8", - "statusBarItem.errorBackground": "#c72e0f" - }, - "semanticHighlighting": true -} diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index b743b1b998..cbae5efd22 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -2,7 +2,7 @@ "$schema": "vscode://schemas/color-theme", "name": "Light+ (default light)", "include": "./light_vs.json", - "tokenColors": [ + "tokenColors": [ // adds rules to the light vs rules { "name": "Function declarations", "scope": [ @@ -191,6 +191,7 @@ } } ], + "semanticHighlighting": true, "semanticTokenColors": { "newOperator": "#AF00DB", "stringLiteral": "#a31515", diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 23881ae8dc..21ccbf71c2 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -1,7 +1,29 @@ { "$schema": "vscode://schemas/color-theme", "name": "Light (Visual Studio)", - "include": "./light_defaults.json", + "colors": { + "editor.background": "#FFFFFF", + "editor.foreground": "#000000", + "editor.inactiveSelectionBackground": "#E5EBF1", + "editorIndentGuide.background": "#D3D3D3", + "editorIndentGuide.activeBackground": "#939393", + "editor.selectionHighlightBackground": "#ADD6FF80", + "editorSuggestWidget.background": "#F3F3F3", + "activityBarBadge.background": "#007ACC", + "sideBarTitle.foreground": "#6F6F6F", + "list.hoverBackground": "#E8E8E8", + "input.placeholderForeground": "#767676", + "searchEditor.textInputBorder": "#CECECE", + "settings.textInputBorder": "#CECECE", + "settings.numberInputBorder": "#CECECE", + "statusBarItem.remoteForeground": "#FFF", + "statusBarItem.remoteBackground": "#16825D", + "sideBarSectionHeader.background": "#0000", + "sideBarSectionHeader.border": "#61616130", + "tab.lastPinnedBorder": "#61616130", + "notebook.cellBorderColor": "#E8E8E8", + "statusBarItem.errorBackground": "#c72e0f" + }, "tokenColors": [ { "scope": [ @@ -386,6 +408,7 @@ } } ], + "semanticHighlighting": true, "semanticTokenColors": { "newOperator": "#0000ff", "stringLiteral": "#a31515", diff --git a/extensions/theme-defaults/yarn.lock b/extensions/theme-defaults/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-defaults/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-kimbie-dark/package.json b/extensions/theme-kimbie-dark/package.json index 7c7ff5ea76..7c186403ad 100644 --- a/extensions/theme-kimbie-dark/package.json +++ b/extensions/theme-kimbie-dark/package.json @@ -1,19 +1,25 @@ { - "name": "theme-kimbie-dark", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "themes": [ - { - "id": "Kimbie Dark", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/kimbie-dark-color-theme.json" - } - ] - } + "name": "theme-kimbie-dark", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Kimbie Dark", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/kimbie-dark-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-kimbie-dark/yarn.lock b/extensions/theme-kimbie-dark/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-kimbie-dark/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-monokai-dimmed/package.json b/extensions/theme-monokai-dimmed/package.json index 43d950eb8c..0c5b4f5fef 100644 --- a/extensions/theme-monokai-dimmed/package.json +++ b/extensions/theme-monokai-dimmed/package.json @@ -1,21 +1,25 @@ { - "name": "theme-monokai-dimmed", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { - "vscode": "*" - }, - "contributes": { - "themes": [ - { - "id": "Monokai Dimmed", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/dimmed-monokai-color-theme.json" - } - ] - } + "name": "theme-monokai-dimmed", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Monokai Dimmed", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/dimmed-monokai-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-monokai-dimmed/yarn.lock b/extensions/theme-monokai-dimmed/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-monokai-dimmed/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-monokai/package.json b/extensions/theme-monokai/package.json index b21aded1b4..b7cffe26a6 100644 --- a/extensions/theme-monokai/package.json +++ b/extensions/theme-monokai/package.json @@ -1,21 +1,25 @@ { - "name": "theme-monokai", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { - "vscode": "*" - }, - "contributes": { - "themes": [ - { - "id": "Monokai", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/monokai-color-theme.json" - } - ] - } + "name": "theme-monokai", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Monokai", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/monokai-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-monokai/yarn.lock b/extensions/theme-monokai/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-monokai/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-quietlight/package.json b/extensions/theme-quietlight/package.json index f2e66e7a95..ea5387ba75 100644 --- a/extensions/theme-quietlight/package.json +++ b/extensions/theme-quietlight/package.json @@ -1,21 +1,25 @@ { - "name": "theme-quietlight", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { - "vscode": "*" - }, - "contributes": { - "themes": [ - { - "id": "Quiet Light", - "label": "%themeLabel%", - "uiTheme": "vs", - "path": "./themes/quietlight-color-theme.json" - } - ] - } + "name": "theme-quietlight", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Quiet Light", + "label": "%themeLabel%", + "uiTheme": "vs", + "path": "./themes/quietlight-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-quietlight/yarn.lock b/extensions/theme-quietlight/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-quietlight/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-red/package.json b/extensions/theme-red/package.json index a9920fdfd0..176c78c22f 100644 --- a/extensions/theme-red/package.json +++ b/extensions/theme-red/package.json @@ -1,19 +1,25 @@ { - "name": "theme-red", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "themes": [ - { - "id": "Red", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/Red-color-theme.json" - } - ] - } + "name": "theme-red", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Red", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/Red-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-red/yarn.lock b/extensions/theme-red/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-red/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-seti/.vscodeignore b/extensions/theme-seti/.vscodeignore index d9011becfb..d3bfa3c35a 100644 --- a/extensions/theme-seti/.vscodeignore +++ b/extensions/theme-seti/.vscodeignore @@ -1,2 +1,3 @@ build/** cgmanifest.json +icons/preview.html diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index 66221a72e1..1fd5801ee6 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -32,6 +32,7 @@ let nonBuiltInLanguages = { // { fileNames, extensions } "haml": { extensions: ['haml'] }, "stylus": { extensions: ['styl'] }, "vala": { extensions: ['vala'] }, + "github-issues": { extensions: ['github-issues'] }, "todo": { fileNames: ['todo'] } }; diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index 4548b98125..3c3017b061 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "4bbf2132df28c71302e305077ce20a811bf7d64b" + "commitHash": "9c1c29d6e9358f9ae99bd3a4bf0d2fa804dca686" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/preview.html b/extensions/theme-seti/icons/preview.html new file mode 100644 index 0000000000..36f1aa6ce6 --- /dev/null +++ b/extensions/theme-seti/icons/preview.html @@ -0,0 +1,104 @@ + + + + + + seti font preview + + + + + + + + diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 82e4a516665550fa3a2f7da6d0ded7b7e6dbdb4a..cd77f8d15aabf811fa16255ce8384ab7228f5014 100644 GIT binary patch delta 33720 zcmV)XK&`*rmI8#B0u*;oMn(Vu00000j06A+00000)XKYvJLZDDW#00D#m00Tw< z016_L2?1hfYF`or2WD$#t{YzNNGM2M~m8@blYgo&=VqMP$HWpW#*vuBTvW@NRU?;oS z%^vo$kNq6rAcr_ie-lSI$}x^}f|H!$G-o)=InHx|i(KL|SGdYGu5*K%+~PKOxXV56 z^MHpu;xSKn$}^txf|tc#|BBbV;VtiI<~<+yNDH6%%oo1$jqm)RwfF+-`2VLiLBj4)*dSN_EdS@OJv?#WZp+)-jfjnLx1`2Rc#cRPZ60<6`4;HnNL@F ze}>BInIijUiR_y#vTu&azPTd%=85c^FS2ie$i9Ul`xc4pTP(6~iO9aCBKww!>{~9f zZ-vV5f2Bxcl}KZ?NMnsiW35PIok(N7NMnOYW1~o8lSpH;NMno2&uEj7wPR# zxp${Xdw-Wmd$&k?k4Ss3NPC}1d%sBgfJpnGNc)gT`>;s6Nu+&5q(M zqGS zIjth+{3deF?;_{?A#%>2BIo=ia?al(=lmmb&QH<$FOoatMF0SJoRqu?oFrFyCz$V! z`+vx|GV;!h%FL>+JJ0H_?kaVu?{0O|Qn$KwH8c`5mJpJR43?m=Ktci=S+WJPVI*M1 ziWzKyWNa`Dve?0+MYc3xE}JnnhJmrLFq(m%YzECLcfS{r)h%{m_qSDfL`J-L@s98K zf5&?{oP$4t$Wrb^?#5gW@qi01EF}0WYJV=`cG48wM!b_WJMGQ?LBG8N^+)~D8X65( zQMZP|UfAodqhU9UkXWA`IKN_C<58=uA#P*rZ49zplvMp>+ zjZOG49o4fv9y*@N3E|4N>DZ}h`OxItQO$C+qp0Xgv81+Dq7tQI6)3tp^h=7NIDa<5 zf~tOCE0#&nHJmRk_!cn<=PIX^b1smoI-XIQYEQ>D^H|x?46l8mCImc-s~RVi>kZz{ zi-H+K+Zq+yLge4xsO^d=aO zH5AH02+>Pa;itNU+~F|OxPP7BtL6nU2tjj{7W^u;OqDUDp+(g@VHhG%+jfCsbZkIv zL!*?UM74InoCDc;WpbiU-8>Txr-sF-{*V^7_> z(rvI1Oh>e>!**ITu}%s)qi&^na0|j$~0Fk zv`eD`d{Ys$l?n`HI-V9el)EAKj@%!>XpV-!XeralaSOUY0|CsnkPH@e{3M8)a3z32 z2@(-R!EhaiK@WaO%J3t36EGoM?gc%0AC&ElhQO%s8g+*wpqarin}Z`nF7yUWY3A)YS|Y+ov@?oce@5#?1hAW~|miZ~^j!E9kLQy1oML!UR>eSc_6FY1QS)=m7gcV7fpgGvA*(??~5fpP!0E(kfL+EB;X_iBd8I;UV zffgb*DThuONKG+-khC(KIu$nSi;eofi@g;FkM~tUC^J0h5%x=K5RZFopTjbpK zfIj5-3~D+soOMy_@}<)!tdgM-6RXfG%q)j#8f8pX5tP6Pl&cN>1zDAMiyDDJwFziZ z>T=MP1cy;Ryh26H;E1YHnh{JkqLK~TG%{)qRDpy6I@Q-4RdMyAig~Z(U_?tGccCy> z4S(PwkfU%cbgG?dE;JfT_0MzQSD|2PV3VjCRy;#X7BSmWUjg|6e2EFP5(Idt#+spW zl|mcEwQf1@7rYhdhrp`Jbjenk9y){{0|_cX2PeQ&L1O@FI3b`ISzi)*)LQ_xWFfzb zK=1)oViT>ubkC_%>+9>NIGC<2YNwU{k&|x>27hZ#7CtgPK=)sD6)KM3jZR#(K0Qpz zb9=Y+E`9B#-QJerrmV(4$<5^s&`-BtwdUKc_%Jsy5t^2aYWr%Qr)l}a4c~LD zx?Qkqo$A4Z&2IDH!R!mS_jG0VGF(@H_8qJ-_&I1SFqlwC{_pTqjE1!Q&A%DE|{!7Dxtcs;$wjXd3%c;3ASIJ!h^#Z+) z222DkkBewD8KO|MJ7NUtzCBS-q6wV|LQplS3;~SMlvo0UO;-v;7-p%`(xM%FpB4qT zjc@z`a^4TH^2?4N)~fB6{;z}PO<%_3cYroc9>%4ipsWmJ*>)sL0{EY;?KUUY{Lb=fJ3i{0btaCD^#)i;X-dt|aYlc`M z7{`D+%otP*0!3H#Ta>T=v}Soy7lG<8m_N&H>iZ@9UvgV=J93vws!cXQ_5zX+JJkXb z_{ly3pq^lHC_s`_X8;gSA-9y|t$!4{a~K&A-Bl^pqn@k*z0m3xw>0Bf)6+HEpT1y^ za%AR)SDOVt4z)nTg4};t@U>Ixre81T>-m`zuM-SyMfsY8h4?NsS)b7>H&+eAQ!SjF z28pV3j~GCf7v~*cj~L>0YU+0VaLt5az_{&^@prgKRVSb9ivg~OOk8u)wSPR05n3VS zB+-ac7NEh6l*H0;*h04Cf?TE#8**~ps#JH4R2jN`0%*6EO@G1w(_dyy_sd~kbcmbIMRgFOxJ%f{s5H$g-lfn=^ zfBt=7Ul>t2s1i|2{DsmPfd64z!mJCX2ui^AsT0D}MfanIps-zA1lIAy{@Cq8B z@3)n*va;9?W>ncZdwo!iDrGQ7W_ZMQT;6mP9T*s{SdGf4J7CUVqn)q6?fNx@H-=kx zu3cNL{|sQu)|i0YQ!(o`?L^on(}|hgf72N5B^G;GWiNXQ2+Qp0TN+hL#@}7Pb{*}! zaYI&$(FMbmRie_C6nTuOCQBkKrd)a2CW?3jo!8qwQF0b=+y%K;!-&b@sUT_nrb7ez zI*hV`$p*^WNCTS)m$t^BQ!_$y?8~%+}2E(IwX|YvF98l_v+^e$no=?bBPP)O&Z|(oSZB z&^0hetz`M)#X-DUuK)ofbkX_-`hmAp6?qfDieE4Oad^T0_0SEiIv5KEnT;4Q2FWj7 zywsj*lt`hx)y!Y?_Ct-@Nag6se+xz-1N8$(WDk06zh~e~Cs_w1xpr zxiArHSPrp;$uGPHq%X}D=0yar?Yv>&&HZilKhIu!=5S)KziK$Ta!Yvl^{?Gg>RmWB zymH>1`c(0cr%oQ~TK>a40k|pw0|lOKlxDA7xQx+PEv|UB#21Pl zXf6mtM38Fk3#Ij|2tzp2HnZG%d!f_!2JuX}shJ+D&esJ+NVoE3p#d3VbWR@^NIrLe z?jfm<7ZKQOeHe{3Oi+Of$VSaUTOLRgZJ6PZbRe3kwbzj)EU+p4By%OE*X60ycm=c7 z=hB5C_3Aq62C0ILq^Qfx*%b6hR|go9vV?pnh&y>O%8cLiUz5TVIU_Hs1h5KJA{>wy zzo-%pMw~&jO5g|4|7HF};5FycPxSOJxsx6hA^}&EK^0GbUY{GRt`4?ix_WAd@3f_v zLI_v&WC&)8pI7z#k%phQt%hB&YIFAv=2lkb$lBUqV`H#Mr#r}vVD$lz1gdjc%k7Xw z)DY-}|Mg+CAMJnm#Y-0f4( zIu;~#gX%|<$NZx2Z{hBE`okao z)-QSy?WM2dxi`+t%nUZ))6>&Ap3wZLT)ONAmFTc{~bn^n}UV$d41XcWMIHQWb{fyi|0mi68J-Se$RA=EWojEeQv z7C3RiTiY{uKGtTncrGT6{XatxS*p-z$byEi-Y{c3rUJBLZiyUSKxA>=RCDs%VQFP7)=5 zU$u3zDGy;@pU-KzJoMcM^yUJ_0JET<9u4ugAN>6feeyjA+pV(?tq(6fj$VA*JJ1Uo z)ho7tWc-<<=dod#l%NJVlDSlpCQabvz^@_T9%<)+cuU$z14$V=a1klJ^=hT07p#vP zS1Q4`es*U3b=_A&u?MT__=o7kvp4I19K9ZQ-g32SC@1*;<{H)DTW8Q;gJHD?jq5k- zZh%_j_nv<~1)bLdq;*-Q_p*ObR>NKya5rM2uQstJMD=qB@nCvKxKcv@E zWsqF}VFB20aa-324Pi2>HNE}??Tdcp5|99DY|d?~Sfiin%&D5c_WQV7+|n?A3)3g#g4k6tR&E4y zR3koC+d#BguXQV?C7(9qw!;-^Ee;>b&B(^l6Lz9+6U4bDpFY1>@4@snOK<} zpSnC#Q$E#mttJ#sUAU5U24K@q>;dT_h0#pC__K(oP@IL-BjoB=FGijEHH3sL&kWU5 z+=>-<+rH!ZYHg-e%s1U~Y*^!eF2`WLM8pgnz`Ai0Q2JD@T&lX{JO8kMc(gqW=IpwP zIR^dVc>ol^gvVgg7hKSXRz(o5D@H8>ONc5FSZkhQsn{sRpn+Ot!PT(bA~YU1&3*^)*fc9P|Eu&Ovr0>oz_-@H}JCVX9_42^r!hkkm zfG74;Z_`xGMz_s2-OnO_b%uwTqnx+Vlk4fU9%o;v(x#|QG0%m#bP)Um=jGvC{48Z-(6!=P+=xU@RFgtsbX z1-~-TDlE_(wY`y_>C`IuVQ+2c(lGIy>DI*&rq%MEiHsZLuY$z4WGpfeQP5#x0E9V+ zo_p&4`=7%8Q}2I&|5G^p?8)12Klxb#U*z^HK66e!eu*4`8J?CNy~!MfB7@Z#QD=>Y zxg@Fh=zv(Rz!=m(wd*G3?dnqdl1@LFb1W}E-M+E~@ND0V6A|&4Gr*6QDsFu$U$3^d z9PZDCd1Ba8^ZPG2cHr>7>+EE1Xj_!x#ynoU)_P~@4S8dBKqF= zyZ8Ra|5-=1m%e~@BJ0EV+per7KeEjU%+PV66fIec-ZdXcR z;x#^IANU)x{mI(>-@N9UZ{A;%V;v}e z=@FOX(z1VUgu?5Cc{o!(&{=I zFrI=J(8W>~-+=b$kn1XEDki3zTNgi!2HtKtrEHvQpOokn6 z6-DtvZe4@XnY+T-a{snlW|hw(%Cvx~RAF4xjl5<(;gjPYf`5k5t6;E{DRaw$IKU~A zf83hWgvP{ir{n}~p>UpGzDn*$b8#9V!xI2>?yybn7{778F}{{Oj^~KeZp{B5-@&IE z^XL7LE`A1dZY#GhcL?rGCfKZof;2^99raSjIsB3sw=-D8f^~-@uP5DZ>3Qid>ZfF$ zh!m(us|ySve1G2=gHQwr$~x_F-8w^FXX%Fe9>X*Q;^n#7@xKXb`n$cm4VNjUXUe7u zL`0nTc$&xKvp?LGNiQ)KrxVz5ave8#=ynyOS*Lk+jn;cP8*N$Y58Ldh=y^#Bv&g>?ATMnToXT2(ZHhS*&3YCbDQ2LfL`f z0eDUn#ClM&2xmIgq+z6s*dzSFe(0j7+P1;KsDA^yi-F8A7N%my7KMF-vx`o4ed0mCF9DXxJ#oyFmrVJH^BlP0DVn+D0}!GZ!a z2os~;fNqqkJmigz^dH3pX^a=j6hxL?@nHzJ3xTbuyKDFHy|-L!*>U{F!xvq2@QpFk ztbc>P4Qe__^~--J#{p}^xlV35cQki8_r}~iaz8@~3ef=VMVFyhqhCS4ir$LejqXJs zkhW$zE^+{cG#Wd8*d6v|UPvdkqWQ#^7xmN34}e}#7fuKA8kA|Kf;wrA`s=7WkaB+* zbyIJClEuilOhp3pKWNJwB`{*UQoX`p$}l*=8VLiEbkd};^>j}A(w7N@JISgW4&cU7 z8W2mJ8lzMh$nP}Z5`@6Zei(si?2D6+B8q>8a^4n-ISoKVEmeqN8SuKsFo7_6o?)O< z1zVQM0|vkd(=^%1G&QV%(qn?Em^7IR;u~;+#xW{`b;S@f8Tu%voG`4rqN#ys@C>1< zmUPT=u;MixXn3(eS)q9N*iuc=ymsCUhyj+b?SR?NRIpZMBnwlm%9x{4iz?jYoKkRBtf!Z`IIH^wksu7vbWf81t2q#fu8l%vK$)ZW--sQ-U2TAIUf&YCh1LPAM8BR7z$Qx@3QHDSBg8 zA}VDBV~dEA=hrRW_MxXYviWilVph#}OzRYY7|`s1S`8p9 zDiflKh|jW8Gr$VsMC!;?5`oaT9E2*OA0$Y@`1|ScUlIiWXVUD$lk)g_r`V*F>e}@5 z0%{cJYsRI55<{cnA%v95^5I>*Wkp$7y6D1g;?m#RIXA87i-qko2d;lp^tIUw6#FW~ z2laJ}M}?`W0j^>LV}ne~FP+Q~9TSaQ4w@6_6trovIx0>Ro;V6G`@(RP<@Qbu^vK$t z?dz7&jHcQDK<%WrZ>^pPbKly@^*LbdKSo55MpLoYi;KGBmComByn#@#W?m0cy|WDR z24v(Ym2keapwkF~KN)|tNzP(=zL!V`7>L*9Fmo;fV#vheDoXPkx-v$7{x~~7I0xYI zEiKCh^8K@L`}Ts&v{OkwD$jHxzvQ;JC)>C0J8bLfvQo1UXyl>~q)KVC6GqGC!rodl zK%4^lM`UhwjtbhR9MQRLa{#Ytj|t@XofD0sp~>t}OSP=8M~;6L!XVl(TjuoEHcX@N zszyLaOEj!YEk!YG(5t}hI;j;*!wiphFTL_cU?5uQbSji@eha!|d0S;;_h>M^X%Aip zICDC8ICqmonQ63a(#OpEH)_00tE5pgP*j^C#vnT@hj;UqW~Vz$6IE7GtJlr4?_tnW zMIm)jM$Kv6pf7*X_f?|R1T<6tS2K2b)N*}{z7)1!u}q1sB76RFzw^9%;akq{+ zQ3ZFKWm^TXK(Oh6q$GIvG)SqVFXaz|0_Yq8ndI5hoS?H?K!_MjFvA&@5Td9~RKTOE zR|~48UQ%e*+H>7xS5oLzn$eW01K$HQ(g{d>%dNs%a6a)zwC4MjHf^x&Vp{|uBc z$!AoVBx*(EQ{;-zkjOIFvtC@a-h*yL4`TWdH<#y4ld4C#MvARg(GW)&Bc?gKY{OMy zWLGa{ldJqADf1ZFx?j>k)wE1h6v?vcwk| zBu*i?y*_E22;;)O^j>v5NXtwPxy|QDHvdM!*&?z6bpP z5inGX1y1c!6gA;N76;_P33Vm;n_3{(odACl@fcBv?t}6)J+RbOg?KSAhxGpk%t2NR zrI_y#7TDBd(9}906;0?x&CrZE$rD9oM(h-djbhmp3n0K400=@=;6rsz(*irl1FSOL zST7Iq!T&OmN%-HAw*#8*r*R>W9qC#NA!GS+=Gt-=1K3tH*-^U^aVWurP#JLf~onYkHeUS_2$Z+9lCVC1K!gF z`Shm^aRPG&Rt@F4v1n+DPz_5NXw)(vfDXsUE;@7s)wNhX?ShF*h^cBkUW|3$pbhl# z;AHsy3Ikgz4=}^@3OB*$RlYS%%UObF9%R+6(kONo;)Qf zt9d6F*!XD>o6UDk5%ClvGYCKV>g&jV+<6%}^du)wVf++9&3C`_6h=?OyYYXn=HgRN zVx+?Rwc|$+g71yX&-=EYA^F@Dx!=gv4oE{@Mpr?~3>Op%Q9(mA z{h|bfMCK{ibl_e9o4adNTty3j0|V2;06snL7`6pHk*hs02R5My7ZB)la_#*{PKOB4! zGIj4%`X28hZpC_ly!jfE-qy4L7tx4yA`yvJ)FIm18hJlCdlo*<9bNV4bK}F?fB5L5KSY1L`W$-m z&tFZC?tXOjx#w1&dUX2_f4F_qCyyTkorYl^Yq?x&5J_?tZsG#fXUiR3xyq^A8IF3O zAmY~|HC8#gT(^H}+YW3AFSz2=!3z)f?JKlxkALzq2N07SK_$Tj{f15o1JpSK- z^30!$#dQYJi;v%Z_v7e=&)sv+=dkza(X(fdK1v=vcJ>MEu}3E~65_AHlMOC535L^> zvrg3EuMCq+E*O8CnHyBW;MayN-5e`5h1W!DuEvR{z4``~8r&lXS8u(-!XGt3)oSY` zK+){$G@DlnPE2f1(_Nctx7DWiHVz+rpIr=86%<{2WS>@NRHdx6k~y9iM=WZno?`~r zshhC~3qKDty_mZocOZAUQu29EWofNTv7ky(Z?!_U9;qx%rk7j@~o+YJJV`0Y7AIvo)z(`ez~ zR)%cA-|T;B1{cZ3Dbuq})i6B(IUKLVAO@EwbWp)R0Xxmec|hkml*uALY{@jChT9Px z0i)7$8~6iJqp{d%jQ&j*gl#!Mrno z;{Zk);NL7T|DdzkS~Ab(8&_^^RJxZRIuUYhc69O9h3>DuVy7q9fQ_Hpw`2S6y*t+T z@4Y3N!uz+cfu)jKEvMs}s(IzduZgPlxoRCd`}Z#Iv{ZFv|7&Yr_r&(2b)RX+c3uI~ z(iDH{_-|Xut@qxVwD8{TJLLCifLZ8iOi*lS_j+;sQhl}#ub0I^L4L%!rQ8n5VX4=6 zl8T)6d6?-?lxBp=WdxBlD?q=7GXIG*Ei5y<7XTD`i%l=yfA#Twd$-J@EnD{P`vZA2 z{$kp%U*CIl9kAa*Vu*OpE&GzyDb(DySAIXP;B0Ds z!b0L=9EA&((`fwpISlzP_|t%8R?=J?KV7E@MsJ$*JXs;qPuHDGce=<(AZ=rsP@m<3 zqHB&%H8$)iU4@5R?uYYW2UV-(y?qYl9K&2l0__60NO6rX5 zQtkYft|pYA9T`)Hy6fI<0>*F9)K$kDcDl6$Y@eLW%Yt#*OeV>IYXAAkNLZc28Q z)+4AwKB35dX2<$bvm$*_aPJ6oo=jlQY6C?YdeWN!P-?h{(~v>-(Byn7xF1BB9lMzb z6HQ`e!DRg|yhh#hyO+7QhNESf<_yCv*C|BZWtjHmFvD-HG<4Bf${vte-)W`GX%d4B zk%JzS=GdlRvW7Cg9G=g6*(F>yhj`Y6^k-U2I$&uoN7PQk?b5(b6Sbwwq@8hk7K+A>ORF9JsjF!e191pf1sQ=Aj>N2D zhaoRm+)z!a1)U1Rd7)4_p4cuiP7U41w_!ULM1-Dg$VI3eEENpAY7llOF9gDI+KT5J z0x`!%q&Rbd78d8`j7Y`6rg9ZHVzJ_Y$)~G$o2NKLY;Gn3-3uCC zSTbb}V<(TnV(zZnITFrJXm4UcG zCLjq!35#sqZG>Kww|okX4`kZdvgR6hsnTPn(1GlWqR+_{xzcc9iYgZ;gN397m8xnH z#8icUkzgs2F#rSxnPM(ODQvkSF#aSuY zhKtbuKyp#L5#;k>zFLn0x7ypWy(cEQVTbVF=Dw2q>)iKp|D5~pxfgOT$;|CdQI#&U z4L0*p!yd>&P&%2Y1HmM~`T{|h@vIahet#N{ltwW(i0s~a(yrsgZqfWHBv?B>w7N5yY9CQhAfMo z_etY15xJ4f;PBF-JN!Kc+)qnJ!tIJ?0Ug^bV=*d8|V^ynCWXRDE&G>aw%hho-39XR^yl(w!+Q4)`k;`ENDP_c`?hw{jI z>DwqjeqO$D^LyFX_-%zXo%3pQwyCYywEVMP`rJGXZV z$hbitY|cR%zO}NbSZ6b@S{4JKELYrSufC*!38n+wlfq-^5zj75P_#y~%n~qj+Y7~PAZ+}e&GeA6NVtLfY`{@4R zB60o8sxd8_nz3`f;urE^pr{os*4>Z*J`fh8C2z3+EbVC~%ddfEXM#B@W4??Y*o>%D zDodSd1_&llOCa(zZuwwr!N{wcM$}M~=^PweTm=b2u^QKluQvhONIz~wET$5QbpQ?; zlHnuJr;JfLU4JcXlcDX^-3-1$eh~Ctx%wYankl*=J@O>MCwn_dtCXnK%7RME)AKW3 zQ1JO+SJEy+drS?qC$v&nk!vz@nR(2&{NUJDc))DAbfzEs3XetcOv%~l01Rvm8$0$i zB({ppfYWs8E;ayQDK-E-MY?l+ohrAa^|D(i=NjcrO=$4ANbbK%At|T*CDA&9Jsy;Rs4F4cYp=Q6_4#btF+I+_W}i z0-7Y<(SI6Fh1A4MPty^kv;JJzE%e%nTydK1`6R;<-NC4zJ}TPGPY}Um8KumRmicQF zAqdufnyY4`=44wppjUwIV0U)?poXfr8*aMCGVcH$%Cx#uq9T+?^Dp+zDv${PJBipq z=}==Urb@#vijYfulPzO_@&UT?`h^MsMWzs?q1L>^iw0E8 z=6{!~CY;q&GX`bNtbyh3bHigHF$|%*2B`8p^gb@~IwhsWg#ap)3pn>5i5Yu z6(-{HwBJIPNxh*))vZ!-%3(7|HD4ImMbI&d64qq!gvfMa5?FFK92MA+k6*PR?HwYO zItf*HcrDbzhima-9T7tYUVuYI%-5U^yMJ`8Oxieg&R_K`0xAWGwFkWdtmkXcThKev z@1c*PKR}P5KS7^Ee}?`XJ%j!l{T=!*=vnm7=qG?JUrOy4uo|$1JwUV(F5nnfaSc!5 z7H;ENyo3jM8(zn|@P2$4UxH8K%kbs+mH1WoHTbpoI(#F(3BMV?6`#d#!@mJG*)`ko zUHD!2xA8srefWO-0DcfZg#Ru6IQ|5F6n_eT8viMN9RC@90)HO=NBksy8b5>o6aJEu ztUW}3ytbgpL6e8;D04~jq#?NjemCUrM$<-q)N5>Bll#&n(p%g<=QUo12Ljp{cHDFm zyfARH4c7+lFoM(2GFM7~fh(D681+DM$o6;pWGE|^o4&Q-p#Xk2pV9(&?53`39t_%9 zr4gJPr01YWGRz(<%Y)$$)r4s^B01NIc0xjb9=Lu250^J4Gim8=h~&9$Kav9~?|~Wt z`a~7E$&VOxWCzM0&4i&*ur!djEcKF}nEXhOyYP`#nI6e|cc7)8J0~j%L?T0B0~^UR*|F#@C%tv# zFAI3GT#qnXhWB6zE(PmoNx(1ol2^}tK`E#KZtF}=ba?uW{pCS0T22d`D~-_$ICX8L zmVxRCocit&YJ$K>2vvkoX_}8vB}9cSD{~l`o+=K4*y1+DF*)waTt2bNq&ERX(sL

C&wRyW`8}3X>|&t zLuuYAMt93m388h!?V_zR!4fKzszyn-Bau?{I6?DrJEHN^CypJ3yDmN~4W>i#_WcxQ zhU|r(7p%$OzkQqBIwo7sfzYqW51ETkNRXGhCY`LwMU<8s!od=}hqRmBCgu3K3J^Vv z$3IN1`7{BNe_qAn0Vo33-+z39H2ESF| zSv3V+tVWt}o~_EHZ@ia~eJ~ps8cUOT z0394ZB;<3(Z>s~rDFFZP0D3?nc>o}%q#)dak$Epv_V+jZ8pY41UXC&^6PcFtB&O@B zhvo%%;sv(!dCxjp;_UCC~5)bI%|B=jtxO(wmKSKMNsMHCbEr0KOS-i^56=bZv)y!gW zX)wpPke6lL_uD<0N}i=}qNn$t-G5+h=SBN&O{Vsr?KGAab}cQQZ7kx2q_MF4`2Mr+ zeOuDTX#bA&`_Ar1Us>8n3pC-{&V|jjCNjn^cV!T6&y9K``0NO@e$DMS9=h@NYd%HH zcx<5It+$T<5`VoAKRK}-$E299#&%`k9d4V1MDSg2zT#8kpS{5tmAgSZS|ri`GG#YQ8>V70KC-wtx44LI8ee|xV1Pc_*|wsVz42R@ zmQSNQ{C^Vqd}Fbmcaqui;>u!q@k394Z1D@Ba|gNY}C*s-mFXKc5Yqy6D=IP)KKzA1Lp5mM~lH&!ySC zZmW6CadytcgjW}O9W_yH#|TtSm09qn7qmblaeqtqs*O@|Z0n%b@~6Ym(snBM>D59N z6x2&5VMrJO$TSh#io*>SwqnkkO{Jz-4C|q$Fbw)gt;?uO65Fc6R7T8p(NY~XFMKy# z^K_FkCQ9e$NdTWr2o+4r@ z5Gua$jk91>_jWVZ+IQ-*ZH(>i#j4j@Z@1SEthZY0c1tx4ttHoKOe?ysOv3@lq;>n~g~GTVoioBd-JM=Pxh_ODN& z_WJ&{sW^%;>g0Bf!XUD7Vk;hd~&AEZ5GhNe-7LXqF?Eg_N>gd7_kXxR3@h z6k(ISYS*(}hC5=i*8%#;Y`$1=P1`7yokX`aCkR)E?;fp2v8OrM&+|wtPOn$Se;3oO zj|OiUZcRp&H9d}Ae|;3|rNH;`W0TND9RqjXx|8rl6n|$yUfH=4%ziK3>q^3Q$$L#X z)5BhBsmlzIWYFvUvSUEQU>pE}bfw+j#;0Wh?)aPX6Wtyt2GW#55d1$5KK5xiK)Ui- z`DGFl?m3j+^C!jD_&!~kS9E;*Mh(6VbnK?d?=8Am5b166sxhJ8>+oGU0lko`AUeRq z>ApOT0h5D96Mx3T=;oKcGd*+c;V&OQ_Ascb_Vmo$^zj$RpPZS&cfB|_^U_ytx#i)9 z=f2dv@fP@dc<$(Vp4oh6m}{m!$6x$R_RAmn)#JxLN>KCv`-z#1|2zEEbU#v=aUdOg z9TIt+VYZF9@IY6zO~3JqW?;=-anltuY7j3JJazGk8-MXBW46&B)&N;RroXos<8LLX zZ8YmMGquDX|84WU?{pgxx!=kCx%B9?-2P@p1ejrdlBTry;X2wBhuvDXyK=S=zBf7- z>WwzH^vITtrXka++&(f*jggx+%C)8G%5-UyhW^A|oh>Cyy)#|uXpA-mZLd36K!esu zZd;ozNlcy6JW79eb4*h=htk!P^aep6l*UkcZ{X2UyVFlnCD-N?|4#N?pi!PC=4@`J zA-zFlp3;(vYUgv4eV|ODril<{vaI0@Rm)?e5Lo2 z1Au(`xgEJZxhrz7$&+YFJW1I@Zbj2{L)FUpLvA&8Y}2T@_+z@JRop{PJ+_^=>K@9M zHPv|OcgwBzwpP2EeW8DM9d7L#?0DPN?^um4Za#SJg`-kboB8dN?MXW*GQHaFtakM6 zd-rU6t7q??zuF4^uGD04hCg_^;AU^J;9SBNz0fOApVGz2`w z@F+gm)@OAxTWQYt0Z~O>bsT6b& zN_pd1qicG4VLR|Sl6-$E_c~=)nF04W0XS)t(1CL7$>sUV|JB%+z)NzK_4YZZ>YO^Y zcU4zaci+Cf)$QuLFTKvpow+xe$t07^mSso=0*T?55DkQvAdBn?VKEU53IxUB`4D7L z@BxYviGX=VgYYBb6PH9$1fINy`cU(GK>SVfzVB4`oq+m&ukU~Dt-7kt_MLAz=llMj zZu?TRXSz@c^v2h~*cCcy9#XzRhO=M@+yv^XJQgZUEB8qP<~>8cpGk~5?qsLRX$ZG0 zK~Ulb(agP09p@3;5jprEQ*@z-VlcjdLy5=0pn2~GRJ@3ukX8~%SN7)+C?)myjhU(h*+!9~n%EvzQ1<)ZX>ZBSk; z*FEVcUUhVEKG@f{Z!gXH?TWp4>O{vd#rmWY3VR};z_)x{-X?Xuy3wE~Utq-*NgoDRp4rqF}Shg#>>q7?)tugqu5>r(-6eIs5dpLyWSf)1s0?;+(%`a zF15fhOi3j-$z4t9aHg43a8vpW2EtfKC=oiM-!apIA-5}QmmIrf-sYlcSj!7@&4JVr zLkwf9X(&AtG_3W_OAT8`^ao4V=hvQTdOCm6xkgN|Z0KkXR4BV>oU&|Pgoev2P0!{L z@Gc|F8ffV1c+|fg1~Xs?F%K*eT5hl(*9{3(kyBc@o1L0svK7Ks)WSFw#EiU$*3bX# zUt=^1Ir5i($xeRmF#OGOmiTO3Xn=OHuW)hU#=?Ip{Dkrwh8Z$Yp_DlWWH%MXq&I&a z&7y}`c7c9yXvecvCNY!Y8<>eC_ww%JER3dnuwO))4EY`vDuV0&o9pRILoZ62i789v zY8beN5v^^m4eOn)?#p8`S07cAieZ+5v~|$|FN)@)2!s=cyna74%%l~^#qdbCM;02x z)y=h%8w7Knbx|t~N~Tdss>4Q=-T;4>)uX7YA^d<&-+u{->q+97fqn$$9MR4{#j3UC z&B3Alr~6BjKS=Lb*|Id>Z?={f+Kt+ODb1JQzlrvz(7$?AFPeTFHCwrrlau6Cue|SPzkB8{i`VSg=mEP3#u~9mb)Jn8?tTp zF=5f$iO)XA^!5Lx7fYdjp7U3M%F8*qwdj%FS!!}}UQjW)5kxNenzUd}Ozx1e{FIdR zR=qi;mwykRRN?*2p2-yFuQPxBjuc4zuYa95k3Ks2^cTPQigV|F_jk#en{S@n3I}_; zyQH#BmJX8L?MDypY;SLq;r3^?u_$BeH-ea!D}`f)Q-zyU-0#cxRseZBcY3`LQJu`- z#L@RcM>H4ZL6%191$Z33WSH9~8ONhB@1kT7<2FK(D{p@E4nZvbG5&v$>3bnxvk&yM zFwINz&82G>oAafjV>y;5OsW;5wVfBbmW?)xoy{UTb;?b7Z+L=1;J=k3@>W=;nLb% zXm4-Z;ar&;rfI|ri_K2g-afhQEG}f-;^8Yt|75!DPV41yqp@1A$NAe7HMSyr_UEF?V4*l9`(we&xv7Z)S_~o+bZybGqDo{ri)$EY$uogFg6Ny0DI~JV2MMVKmC8c3`o%1$ zRu?Lj8lfM1M0YrMba%U*I$#P5_z#@WZ*SkL$yy9I*13PbF?LR4U5E=j&*teBV3RTnCt_^bCS?vYS~5OQG+roiqm1ZuUs5cV4c zo%S-1B8-3Xu&Ypvi}3v0?~z-`?>t1=bIJEU^StMM=FHXbU_broTb?4?b3gOc^IrG7 z_g^!yuAb?Ggc*f3u#Rs5`BfnKp7jc%1E>i0e(#WqTT+|%dq^L#0{fX^6@dUs)ZX_% z*X*qYa z7TBju&ktPceWTmjFMs${P(oc@oqA{U!l`>Yv`xPNv%Fe3ShyT_EEMyD*_1~xk!i{W z3>1HkXYTx|(k@pB4pDwAhqCCVk(zj*>}4r=^2A*yPTci#_|iJ?-2MBXd*C_40|$mC z!o3-wl^uOSA5>YeGjIH?Qv`F&bwD%oM+XiFLl=a&f|F6iAGil%?iZXMy)sIZG1>C91UiRbBeL3V5u_7_e7 z2fG=lJFq5)T-|?p zJS}0IV^pB8VmBHB?}E~xq&^HupZ;sDKO7$FZq7;9nk#|~C(Hq@Rd8qS8QGyK)9Cpp z(Xpptn8y5-4IA_EGz|V7zk;89SJ(89@tB9wb-5nCC~KmLe}hoPn+E=$-~BAcrID=UK?1s$i7zt8g*FN88D`1oVG;jR-`w zy9jrSg-|M#tLDN6^FhCOKIp#p72XXJ?vcXdg|8OAS@_?De<^&o@O|7ViY~z3C{kIc zWa=t1>BAIuqw5+GVx?B81PM&fUai?k=0Ishk7+iY0#`Vaa3*U3R7l36I5!kTPb^0;Wy-|$)f~=T)5UKmm)hcf+lAR zZ*VF}sV6b&gOrv-bio(G6cr$R=lC&R;lh83W)uw!>gep#CjSI4;%ji+;bn3k(+?e5 zq`GC6JyEeO-Im$TQQLp9E74X2v}v=WaiiNbu7!RlziL)EcasjSTQMf{h}ovsP!1@HMrx+;NEb5|xY}Ee3x@;cdlLA1quZbjd;E zBtxTSh_dtj0=!${ns95yp*VQhEIs$Ibmsd`#{rdVhDE^EKv*N<)&g3S#TFv@V=(ME z*Y5>9S7=x5_@HyEc!=J4{{F|;!zc=W>UUoAq8Hu$+191$#?JFM2P6YtPoSvtg8UkHF zpkCEh(X$-9E0cI53+MvuUp~)kwZ^XBjASPagaKA7#v)gC<}Q4J>*Zw|0LhT0^<3=46I@-o414N+2{k}d}MnljrG5%^aR1VyjP!*03 z^Id<&e7mlMG0!z|Mr1Vn8%CI{3;{BY3Q zR_S2Z^QGkJnl&y$V=t(gLsFH_AR7+W2?pm+oo6v!D`o=5q>On!2)rI$MEZjclq1w` zM2d=bgFuyr33^!eun|f}j4$A$%s@Xovo(L7aJnqiOJ+C#^!<$Kp$hJ2yk;T`$K~Y^ zxVC}NG8M4QY|QN^q^VVy4JYaWre>qKq=vCuno3kk(G5Gp-8z_4p@t!tk~=nTT;L7{ zA|)|&#l(!In$0Zg%a}=q@#s2+A_@%7g#TDGE>x%?sX&i2g7v|OK}ur`jW9W!sOx`~ z7*(DcI?+k4$xv5*dYazG%}8LpAp>nj3T|WkCIPDu*AsE41CtaRTsf9G2&;)H>4=S9 z>6j4RwR8)8>EX0c7n5K9$57T?PnsTd4M)smD9q46(@|^rh!$lMPX~!4Y-jy23vJ!k zYs?O}x}4nf9Fu`{s#}4*Zy(yApW=Tov2Ec{vtmaeW~>;*C-*Zp`FV++p0wgnA_+Zw z6MQ>n3xuDA{yW7U${{kzTXNLQ$rA>?S?@55mMNVt3H;Hde;cCvpqTarHht&gr4Cm)_%sW2Oc3Zc(+%ZCiFu?gK@AL>MCX1M<2o=0VTYTgI;oi&j01ms1rr%M zqXGibk(f0^tHg}gkt9&plOgt=8i@FpQF|enBQ~4aqQiFpokhV&PM^zOx zj7SJ2NMpFD_H>GXN?_|^MDoP?5cGLu6{r+$D~G_%EsA8JJObgHxQUqorl#;r3>ZZ( z&?3V@1)BBP5@=J{%2LB;q2Yg7%PtfO1IdF-OuoX5ga;-_@I}IQfF0XFVPXljVI)7` z)Jwi5<=+L+pDNcWp?;%7>DyK?LIQ})V`_x#$v@+}WzNBi1;hC+VVS=5A^gd+rt;=n z^aF*P^8Bs93vj=(H&*GIC@Dt4ET9a^dc}NY*Fzm!qIILdKip6%dIWyj<8FCfvf(8F8%r4 z2t+b;x65oNb-iA>l#jPu$d$DShaN_oWPALrX}pq2bKWSMd(m~ zX}G%U%^5ECqb36fYP_(#+b+Z8 z0VN^3s96q#R(6=e?`>IzS3EG6M!=1R)s|BU7o3XOQ`oyz0|9#3RY|>=xGpoALFiw| zfiNtfb14Xx7Dj(>_FHbAB~x`1^dW{{px9`|(e#8-!q#*;xp^sx$g4Xqf6s}_pXd73 z>Wfcb{DwQ)BKUkU3W^wT{^3m}$t!p0U+IH)_SMj&~rTulr=Wd%jwo{b$Gr;MBc63B>>D6?PF(9{rVZ{?}=e1H7h)!mYw*Jvy88 z-FA1Kf-W_Fdid!>(mi_9s97!duL0x6j8v5M15jE;#U+ThvP;f}mC@NverE0T4W~EO z;bomRE7kJg89lHMkK8z33OBpxHvRy@8aTWHVqAaRzov8dvYXdAvDfjU=fKOUSq?S& z&4OKs3r)2ucyZxW;jGF&iV#>TKR75wLPZP@K`HFCX3n_c=_lxl#)d68A&v62!A$>w z6D}OyyBB$3HKR1^IaF(lc%QO_jZ$CYRYJ=AFV{@f>^dC+fNZaO(~+ zz6gIm*ykU&&HL|%Grrlb?o?YXcxhi1n)Z$j|7IxdSF1zn$E)D0IxgXaSGOy>c>Zps zy}j$mP&zR@cbyA9eLCYn<*2c{nDI>ImPUb!@b#~gjmei7 z$NX`}HhLS2$Cr*RU0Ba8!aS)Lw$L^Si@1Ljq2j2N+e0z;W5c}8slvQTX28GbgM#Z7 z2xgm(#!oP--XU)VX}eU>1vBfVXmW@8#;!6;tq>G|N6{~<9c;YyvJ-#KLw>NjLRQrk zi^YJsUDuf0K{V2iJq^Awa=l$yDS<`PSt;#X4nnJRw$!?4Z*KoKJFe<{M6DKQGhlyz zg|1THTOFZkCX`z+f;=T7h3=H9Orh$~wu+O2L*&WBMT5C5wAZzj_TaYu#*ze8tkg0< z_-&(+0grz=r84$i@X+29Jh_zV?KzO(7W{O3EEhc+Wa>Jk zI2gtwO!sf}nXXQss9TTZQn8_?$>{pgr zJ4bB0f*CKh&grCC>Xt5DEYB0ys+Yl>e(uGWPd+LgKX5Git*f?AT=Ie|S}=cj+9zLn z{l!;D?xjA}gq~<0z2Q{JH+=yE&hsz}E-$_4#IyWX*V*4O5usdf1_yU5iG$91mDT>w z_@V8^%KXgdf5dP{Tq^&$R$;l&FC0`@Ubs8i&4#-~j7P%_lFTsnqkr=Oj6g6A8R>S$ z{RSC_WSo#kjvs&DX-ZB{u7rP=AGM2?RkWY}N-nx~vqF>GNtB)c>a|-HlH5M|R`y!@ z$l9n!=H^!4wngWvYwt7QblZNh%-`~FF$k$arWoEXRmbb(X}N5X=m#+RQ&fM@3*xLh$iNVB zg1`*N-Oai~JQ=vs)4&#zamCNJDyz0jQPPXjqfPH`U;F?WONMWIBy$VFg;pSKT*o*Yo3VdPB`YU9MFfVRJ-=J&{ zshnQ`ty4->-VdO;u~@k_4EE5_hSTYTQPV23M2=yM@&MO%`)!Qh=ws}FN@2m7BWty+ zk-h50N4l|P$+WYY){pKxcewr+#mKH~4kQmWmTra8=YEiBkr{tTALM9jXT(C>Z;OIb z4ECsD%(-Sf$4rxzF|dhht~5<5yzTyStLknZ%y!zXwA;RXJen&TkxYL2;BiT5Q`0+) z%z@Pt6b;iR9P|Lcy%3dj*=7Q)AFy|!2e~Ev0CWHi?Ohd^rrDcuWwr+Qr_3wNBd)$m z!KB>PVx`r%YB7IqG|rUMVv>Vnxpe03Kbi)IDB8l*GP4EdSY`Lcbq$(z*Jsq1P4mkj zLC!0@Ih8PjoVaJ9WH!e@AYsb$lrt3FP?!TZnFBdQe%OD$0 zmrUbXE}MU8@|vOYk$~Tkyz4BPhWRV74CMlvlYQ>}rK~!YE{IOomTCmo$}ZR`xPp|! zAN2RYh1AZJ+M-Z-qjMw-)*X;l)HiL-(CiS+)R@bX-SF+6lf@Ogq17}_F~6D+6`pCn zb~K|JrZ_Ue?qGI!WJ~yZtOEQq$(RnDH3-}gOdWr5__Dz=lbO7JN7Go~*hM30fPJ!j z;G#`kcWWi1W08m#4t0x5!?CFZ!(9grx+2ZCWGivgUP25XVQ^k^8-^Z3 zy=(?*InlcZmX~>GMy4OYjNx|gm{<2k>#PO_s?9d`nXzZ0U-*g!W~9NmL2Huxy07)F zXXt;oVWK}ewe>0xfmI23jhhwC*LlqWyJ2rH_DXi*X}L<1sM`_I7~dNt>XJ7-_89rx zV~?Fb^w?uhJ@y!V?8pA-Hhhv3@bW=;UeLv!O&A>>Q`)O+HT49ol~|qH4OLejD{cDB^W0l`YI?TrvF4>U~R~ z*GR3ypOhV@>87kW-x&supjie)AM}z)W3(fmXf(cJFDCEu);HuGp3CX+9s^!w`#1S4 zTQcwTU7cOLPnw>8GkIij@$;JP9lO(?mmpduk6cGh-@n`UO?uq4&GOAlk1kz3SGs@w z=+SnEYA-y-1k;x~+OW5}O7xq~n6h&rKc8EO3R&TbeC<3@VL!;wr;$J7VpPhV>A`MC z=Xx~>lrzho?G|HZfGPjNwbE=vp8R-cpI{pu=CH{xZfvh-F}XoH%(5oGWSi_b10K9$ zfyi6hZ6R`)kQ(zFjZSDc?PH+YFlT=~<<8_>wqIul*2qnZ`^4n$J{~%f++drgz3&t- zC@lukX>7#k=*uij9Tlyu^veMaZxc*9Pir4W1Rcj+njZ{mQX>X~h)KS8sPegAXM-K8 zmT`t^R}gc6V{@s3s6n-0LjK+>mApv_UdiI`9vz?cI!m2--wZ0ioE<}0zFB{c4eq)@ zQ0mP2^Yz&Dq@{z#u;4LgdZmYVnLNI^`DcIjxFL*EJt>w;FtLFn0vFUQrbEN2EY;(= z%rSM#5suI_S8c~WrWoXdtPU26SEzyww^cY?xC(5z8$kzpN#X9oI}7hCe6sN03*RdI zGto(#jK~pkEqN~aaq>Fyv*dpRq%bPpO5aZ3MSq<>NPmz1 zDSe!Ng?^2Gm!1c0q0Hu4n|0ZU9b|{uF?NDo!(PSS!rsn)j-6xgVIN>0X1~E6WWUe; zf_;U3jXkB+wYIjTU8bGXenfkr_G;~J?Ty-7w2x>X(|%L?ZSD89C$xXR*1n-z`nrBf zKck=3e@1_o{t^9S`tRvq)W58MRsV+mZ4Tx1v1iL1o5;sxSG;&tMU;w|E>;)CKd;t#~*;;+U35&vpb zjgGNw95jv?mm60bHyVGp8ZS0pW!z)@obevxSB#GtzhnHN@tE;tMRw#^9+H>KE9Eog&GI&RhkS{At$c&LSN^O#Cx2CbN`689iToS+ck=J$lkzE( z<+z+%=}@4HO_a99)~K&+iA-RK(}nEe9-_aE6izNqC(LxKyxD)d!*N&n$m3U38q#qL zOfQ2wCLV%@qobE7_2Y3KQHXj&Z;Ytc9)c)=;fTqwlwyGzNtW)?9@Ztno%VJYPseML zT#XyzM&dvnOAzg8r0gOD{pVsZ-G4z})p^46?WnX3zl@X$vpF1$MM;2hg!@zNY#<_R zOE1PN$GL5#YJq?2oB3YJs1!%NKAgUljj`wmZbe5}VPHR*KubB9-mlw6Y&(G$V%>n* zWN;M2Pm>5;*waxLQ_7FT=Ae%qBUIp7+#SXFq7rl+Muedjn;@p=G^61tQ`Zcqy$4OC z{g{_9^Z;G5qb?$@bb%M-cY!hyq;H%I#?UNWh4g3_>M?&DE1Pi!af&EH8(@c|Y8iJl zL=4q=r6wbGDzI0D{(A`jv9r}HDwLaBW8AL9J7Ox9@c>olC>DsT1l$#4*V+o(WD0nS zPQyqiC6OXf5neMIcYBdY`(xBM;n_ten-Q8mSu)7_qJ+`(Ne0cq9jXzOmm?-2tPEVg zc?gcCJR5%{qht$z0tjlfK(!%;U@)f)`QQ40MVl_CrVhwu0~ z!APJU5}%7MD($?paZxR|=HdEre*_8_RIi_80tJ6khR}&wg0xv9crQ@Vqj4YF*xf3L z;UMitFrZMe5l%2Hx4Q|_Z4C1=#S}Qo^)-jC#V|t5N;k?tUO>x57n9@RG$}z>rJ3kL zPY=LS$`$*5uI}NaRw>bOxxbxpq@FHxk!tDI0LBYcS}P;&3U-8LM=4T^J2C zs``IOS1KSWLyAlyq@yl$!&nUwT-p@~Gpm|;ko2L91iHQ(4Z3Qeu#Yji0QV(fnhyuc z%79koT@0i!j<&iu!x5GiVJ3UysLi|BF`%oC`6%wA2OEyP8m3V;&ND5ciYpZGbvN5W zdRJ?@Kz2YZiQ2+YF`r;V&f~}-S>Wb|3KD-pvk8|=RTn^;a3*I%98C<$8Y`RzMW$oS z*DwYtjDs@j<2=AnAl$|V!sKZ-Oh604a^Yf}tLc8SlVcNa;GNq*c-;g^q=8}RNeMT2 zM|tzdTlpS3cFSlO?~n|^d0+@}fhcjJj!zUtnM(U>oCT%6_vhv}veJGUa~_*Jpn!7Z$b= zm}qx^5Je+IbyjUFVNHRLr2?4+j55gG0s0(f7=M^-CjE8;y#wI4oM(#Zfvmgao3gZRzp>|F%#V~)~0~(VlbtVy5 zPpD=Jv_9SjyNdR)uaZ7;3>;BKGkwfV1{?s0x|FmPsrENju3k*ji<$+d1??l}gi_TM z?)HK9fK6jO$126Sxo6uPPy z(z^*YtY+Xk43`NV*U*2su4{aeao;DtuTjYn*Nzdxry5%(8b{n@(^D|aw!?I(8wO^B z#HHO^aew_xeA*fRh8iiQo5zP~->gdFBGh9kRVbliqd2kL_|&@Vst#7t>_Q^&&rmWB0M? zk8BFRh4)d8GE{xlDETdQy{>-NR9)Up$T{*GnAk+MQOrP+nC265ADpWBfiUd-%41ml zt9yNhEK&CJ@bOs~EC(a6u#}i?L30tYYeYy~gdbD`OIUw3O?x&WpC#ny35DyFSCGm> zij9F%KZ6y6mk*E*cDaI#QkSVUP&Lj|ASvt|_At$VB02Kfm%xrB4^X;@cUdIwfMcJY zcGT~vI>Ynz4ABb|iKtFs%!RkZSlZ{Y?m2>64ln(2)k_qqmyx`m(USy+2_XSE7W6hk z-jX*PsYrj_Q&B!N>nMCUM3s|*uBy*;_D)J?gQwu%nJNxmpcbDbpnxj$0-|sf)SEsX zPaIxo5&2`d@=MdwDES9V^AATrjD5a1#H0Btlgw>_*1kOUOqNd7a>iL?PnN2{mJl1U^ene-%3%EB!_CO<+_CI$}{# zHjW$8=qIs-LV7d^N}w^if6MEwFQmLQyji1<%Q6HA8KzKp5Qu1~vL`slC61TNPnPI{NjR~otJ0Z}!R2d=c z5TpubaL^-0+bOYt4sBt|ccb6ePeDsGK~1UY3Fe0u?9*zVQsUS+#h^`ZlPDUvF>m$eG8=-bzJOgp^04_FJ0iiD|97WVJNG) z9>GuOzu#t<4;G#HfG91T)w*uu(3m=#Cq9g&>w;tv*n<%ZsgDI~Y6y|aX^7EoC{Sw{ zPfI|nq+BvCg<`KB%YEoH;xQr&l?odP+Xcz5&6Ozv>C+N|?0XI{dSb@D4a9!{eIm0E zDx@`<2!QX$5#BCC7KCXrFrjvoAQ3?0<$O^F9#jfubbkp*R8a!A?XZNr*NSZEtGTF2 zYQjA<#R&v0&Wk3_V5elc%*L5uGNY(@frKdwr!=Z%TQhQU7mnv@A*M3H=_;tOsm(k` zV8U-8VxS@Nt4`$$2O82Z_g*3xaQcx}om^}u1ry{rw^aXZ=#szXQVq*>qtPLq9ZcWon0b^hW zvIAX2>#K&_H8D$xG(E|Em+K5elr;mPG^8|j18GQN0h}ZnDq%|8S)hMk=&sOtnIR#9 zF6G#M%QuR|H#(W&0mE1pu2|%lJRI{~RDh67k7JOd3na&Ih6#xX@t0*CxY9<~N!nK2 z4KDV)%RqE!YW{OjH{0AZp@9aBFM4xA$Cq_Y0z2VIs!XMr)DR>M!zguAXZ}Bk&Ve21(2-Q7wlXZt8lhPhQCQG8 zPlUy}#VPY#=qh61{uj;>sWachNCiTCj$j^C!qH+;>1G(91@eE92^S+73?k|XGmSGG zH9X`JI0(Yfgw(02^6n6>%2660{u?G-i=MF_2xO+YHb-_&tI#W@nNJ*Ll zglSs_cAsM^bR=j}Nj1x$3s8bC(Z9^XNhL9FH0rII4MWcY=5a*i#%=&AHMr6cQ4`hi zMli2nQW{(n4v>Fj*#hoCn2UNcrpV$0Ygc5;JAp+4mSc0MVckUD#8GlltO@0NeWC@h-#oR zJwVkImag=9m`6Ix>#KBUBtC^!4BeseKtX39%ch;*EJ9GkLYuAMW7m8nHB<65RJ&(i1Tn(L%TsuBR-srQwo{2Kv{#i zUl|OijhMj%<_0?BV&o3UBBEwFSMwTXAjnx)7(f-k?gc7MP(@ui$17Yh8A8L82up$!&j?O{ilJ>WZzCFu*Jc21si|0Tuy$ zLYb}yTm^d+B_%4jn4Ajw)rMAaL>JR=j@rbq4G&$%2+9mm<|~L}%>)jixv0S+eh*&CmC~fZh;hht6@P$4sH{UwM2+y9s7RwsCF(@b zY&CzI2wzpqB-ougLPMji+aOJ}Pz}_`X|As2it@~Sn0?J(s5Z4QP(ioM^7N!Ut&V31 z8yb$Oob#r+*jbeib-B~I`44K0C?^O9?ocIY=a8- zvdP0_XaD}MSUyMqX0Wg}zp_!QrL9f3WP0T|a5>kFV7yiC+O=k9*hCvSoaL;3lJ$QJ z+l7;bn+iW(xEJ@|_CS6-GIj3C;TY-^gOijOQ*Y0saW7o8ulz63F?|+4haFJb1JoFg z@{Mwppgnh6R~T&Q5d}vuEuKn+GKCq6Xmv63=0J)7vDiFVObmbCVTcw^#avpYrXRG5 z2*gL7m29Jvy>i*DW-F&FwjV6%hdO^*2uhI`>3lWT3{ey&aBZ`L;9;)@Le>Lo zPG^zpOZz&mgMbPvuI0Lh()#IHDV%bYm=-sMSM_xCmjpevXt=Q~g^n$4%pa}0_MqF+ zba&&fjjQX6P@(IOw?k7auWs3)>4DAYhR4_HQWL@qvPNox2$sh@11@JWZpVLK$tfl` zo3@M1S_!RygEHWN3MS1NZF$TsgoQeyFwY!HRZprY0aonql1Lb=dXah=6h zF;z){Sw9)$zesUWIEBw?H--09^q-);#~0VT`?DH8vW-?vcg)rw?%P;4H{|k_raMiO zn{>O$1+)qKNcl*cHVjVx35=5ikzA!v+}-MR%MC`j1bYyYq(0&iQjMzTpgE?=IcWK74fcftYW5{1=`0sx=zFtLIwS?a6@nAL#4M< z1@0#hd?xAV_$WwHS<;@x9AE~a9f}c&n5{wWFhqM8rTrKvaEO0=O;;diQSK9lucK-9 z=V|nLH{;4>OgRQ)>9{bg!fqAci`7<>3NcSM_ha_FI>q!eIm(V=oKvqr-2U1od*G-e zmB5Dl1E>`1aR<@sg|8)KT%-DZOvm6gradoqaXrN}YFZn!y$?0bITiXfRKq}hGA*N~ zw<>`{%M!FO7KML2BE9tT^?Bs=`cirE{2%4(ca99opf5^RnU|?wyy6IX%6(1@N`q0f z*TuN)TbebG=p}zA0()7EJCRGv7yC|CHx9Wfx#3}uM251bK#MMI`*uh!vaFYu`C6@n zU|tc}1gHxE*EZUJJRh&k7txv<&25$qv?Op*iqzQ-m~4N=Mh!Gy$;uUDKAk(cRX6Ug z9uK{mCYqq@62tPWOsqE}H&hk@e}3YCRQF6cSW_`_&?Gkz4YFKJJPYowr(X3(P_|M& ztgaQ6iYBN-cGGg)Yofs@4WYTL=(|wrvB0a4%XQ6R0VoB!vuqhQFo+Pe9i7Ff`~=RX zu|ESw8Ek*qW~3X>@XXSoY^PgBU9vZ~J`Y{Msb?|kM#6R$D<-b-(SU=>$&$M45V{v9 zzJ9@XZJNt`NIFG_uuk`(@52Kq?&`^}k!uLN&@ZRoA#>kJzf9t-$)C~YuBFOw*WG z+&slNV2nR&V{Rpkj8>5ea22NiQVCUXA8Ci}k^%0@w0WnrH7qL7)LF6@-K?#0c2T$Nw6?>!p9w*sh<9?*TxMzNf zKm6u+co`(4%u}b~!MNv}kQ48xH#y)2HR{oKcVKt~+z) zJColc)Y`C`9p-c3V5AblnAXrr<&L>wMxn_qI!E7jm4p%|@z6AGF zq8nJam||5hH%F|88s-+UsNbw(xG^J#hr`J)4ToyG z)kl%8qwvW{sv`B&wCJf+q8MK=>cwQ^_8Xsd8@r0FUUy*M#f&&tnc?LJxUYZF8&5v_ zU9F75YD2p0rseCVs1={aj9S=md9w? zYY|L&FtvKGoG%gUBI{A2FvV7 z;j+Tdow~~G_4gXyhaN)`RAHNl+&m==rrmX@Pxe9XbrtNj0 z?5%MR8p}>iO2RM%`Fk#n$@fYn{m>6RhCBb}@p>vSNz!m0j9TdOI654~$Eg>P(4$^R zf=@|Kui;Yi^B>`ozG(7#a!aHKqT{J21W){FcSbi<^vIUoMT{MU|@bh8D`l) znI2N~<5J6#O}T$RaM?%5edPF`Ug12@?f|I;0Ox;*=Kufz000000HOfA0UiOy0<;8T z1e^s91vUk~20{jg2EYfV2tEkX2@DB(3JMDR3tkI+3#JR!3=j;G473dN4SEfl4k`}F z4+IY)4{8tI5EKwR5MmIn5YQ0l5h4<35`q%+6L=ID7Gi%F78kY{WEl<`02+!K*c*}? z)Eqb*cpR=A${gk$XdUVv93Ge+_#e6-`XEjs>?Bwu<|QU2LM31(m?!Wn!YlABDl9}S zWGt{P1}%gx8ZLq^>SwdNGS+-j)TU1;~T_jy(UKCzPUh;UHV_;-pV3^6p!f>7e1ek!B z3kVq){)71p04#0-jR1I@U68#_!!Q(uPujFVD*>~ZhBg5K4FmH0HR$}>Xm6cw(R-?H z>1m|ys+x_>jrEvp{Qo$PO=QSXphSflTbN)Q`>-D;;Q$WeWE{dNI2DqJCk#8d4%g!b z+=#=t2{)57h%h)2yoi_ZGG4)}cnz=P4ZMlB@HXDTF5U%00}cT~iw->`rWl|wLc_q~ zC>%TjB4(IlfhCUNJ-m;Tn}{t{IF5fd3TCaMin&ghxz3m$3l@bmMZ+3WMPugVvc{{5 z@A#fM3!ZZ}P`!d(+uK9DCq6W0u@m)PN}~R6E9XqfD3C*p)UWZqv>(^hV=Iv41B?;fV22CrnI?s!YdKw7HATGJy{h&lLIe z45!?)ti3S3y0J{6<8EgzPOR<0^nS@e~UO*%!T zpOFfb`B_)^xopE$NFHRXp0bdX4kl3<{Mx&Dn9FVwmi9j(W~qi^CIy*n7Ac>nzbiPK svrx^cvB^`}yi2*%Ng1EdjT6~I`D`(&e94^7mZQp!NxQM}FZAW48z~9T1^@s6 delta 33535 zcmV)KK)Sz#m;&6E0u*;oMn(Vu00000i(CK;00000(_oPlKYvGKZDDW#00D#m00Te( z016cOCE4a@YUZFG150EGkq001oj00LlED*ym&Z)0Hq0EILF00A-p00A-& zape+iVR&!=05*nb0000V0000W0&xlvZeeX@004%F0003V0005z-bxwFaBp*T004)m z000BB000G8vJ;2RlL!H6e=wi<1&|vBCV?731OQU94A%evc%1Fjg;pF{7)9ZY1R^BF z-QC^Y-Cc>hyE`#rL>cGR`b{!*YR_W~tgmj-f8TwfS1o`JKuuwCp%$uJsVeTMYAIf= zDqpXmbNPC0Ra5z%+Pq$A|Mjc5R^?j!Tw7kNi}%(R#X9QgNT*_7e-|3)N;ewmP7iw0 ztGH{VH+|?!Kl(F(fed0WLm0|1hBJbZjAArn7|S@uGl7XrDt?D4Ol2C=%kLS?WEQiT z!(8Sup9L&r5sO*EQkJp2*uR36tYS55Sj#%rvw@ApbrYM}!q#Hl#&&kFlU?j)4}00i zehzSuLmcJ^O&sMIf5$n&NltN^Go0ld=efW|E^(PFT;&?qxxr0tahp5b7YE@_x3; z`#B=}=8Ei_C$ew8$i4+4`xc7qTO_h?vB{}+XZ@I|66(ajqitJk@vTwD> zzBMX8|Ft5Gbs~-RB8?3qjg2CWO(KoWB8@E~jjbY$Z6b~BD&Mn1q_t=SA8VMA{cc+LuJymqprFMA}zH+Sf$d*G1YlMA|n++P6g7w?*1_MA~;n z+V@1-_eI(dMA{EU+K)uqk44%~e?;0(McU6q+RsJWFGSieMcU0G?N=i0*COpVBJH;# z?RO&W7LoRQk@g3X_D7Zf+dqk%^I7DaR*`eQh@A6PK>=nfKtm-&5c2uC6+D>MY;#|CV!K#tBHu!!X(7Lgdfm~m-)To-_Nt*|5w$z*SrjC0FFWT8 z4zErUtfS#**co=(33}nsorezH`FrxCvF+7cx4wGYRfBEY1{z~JS2)JIZ|HGO6;^!Z ze=uFs(=Aiao6d0CHlZ+qkR>=e-o5lSm-c$_(M9vk)!nN}t#N0wfBJQd38jpXTTf{k z;crB%hzQm3xB9Lo=$Roxm1iFXQJ|eP|JCVB?`o{y=QVR(_i<*nLoiqiv5$`0; zPJ8n;=(l&E{-{4%L!;p;>ef)$3wvGYTQ?g63&AZFiry9aCB;x2n_xjzKd=?cBmlk}Bn1pkcQ_49PNL3xrC{4AeW1D%bY-onp zzEBeap2by-6Uy}lZ|6n9454j}iftkE`MBDsTAX5Hho+_re$C4+EMsM}uB8>IKO_B%R1k%-nN4FD-+v;(Zre;y6HDa{1XyNb?WAsa5yzAMlGLOI%X)x>CqXC?2j8JJu8q# z)bU=Ie~}+~>eiKR6UK|044%1h@V9Pqbxm8^va%jx#7`X-Ol2kz2wGe}eaz&FLRhK3 zW63nu=2WJ+YN1^k72unSpsiG3DAVz@$f4X#xwqtgA4YRD1V&4lPL5m91sVuou7zZ< zsN*L=)PySm3`&rQAPR=-I1GC5OHzg($(w))f8la3=*jz_Y;QCKMwN1?I~)Pc42Ib} zETTRztz`COQUKP^VozdCe=P8i*pw zKn5AUDHw1Z@pJ?d4GyTQtF+(80%IB!4_=FbJTYZdm{{QR3-T>n$7OeU`C8)n zf8xOQrNZh?h2W)yojdG&({%DtWpx%~p$nz!Iy}r5YXoMCXpB~DhOZs8=Y^`&ZJ53! z&O<|>DTLb^2t>>=@l>Qx4u^149z;vqqYN0H?&>}DX~d8eomh1O3#-)_I*(o5G%$l1 zZcQEBQ*%}2JxeunDk9?am0Af?O;_iie~cV2!ql@mApaKn!;cWK@Byue zm(D-XjNk#>JV<49s=4|@Aq9Rh@IXWe%pEHklzASInxb+` zXrKTUN;Ojo?~5MtwZy%+(_fSOb!ZGz@tvu3kc;6zXiIvFdDLJgst zfu&gvIc88YKLuKd*rXgfWgs=h07BBraOzaptS>g||1S1c7(Cur1)1HG)+j zaaJwG+H8??+XMQL<1?t~z;M<@t;?5ApRh`XMog?iuQ0P5rfHNhRYgz&BT%k3^cQ4R z-Yo)N0A3-WMXAd{R}vgXfBEnV6)}S&s!C}_FxiMoHfYnxs5wvt5(em0UvpH&)r%_T zy^@0wErHyH!dx|gi$IRTvCye@s=3f;EY&~DfnSA!sew(RYFP0MEm_2DOMM082k<2( z&`J>Cp&Dz3%2f(&6xX@sykGEEpdSLOD$^xfWqRlkejFsI03DnFe@_LC0jS}GfMR5Q zN$62;0n}0o*g)_BRbmsZzjW`ZQ|s&Ns5qFeEo!Hg{*i_5iEj7AwI>T7njWABuDS{p z$L~NVu3DcSCgr)kTT-mm+cMmg)%eG`x!i%=VbI=v$+hx9BGuWTs4X1$z%Kv~M^VZN zn*genh-8!udg;3-f6_MyaQROBQ3uX{v$Zy#j1~{ZyA?;%d@Ia{#eB`}biKgZRe=Z0 zF3#3%Loq`1OwDMU?L|JlV781Jyr;W-rfar@Tjw?OlkHcn`F1Nl%uP&$rX{1=zMAK0 zTK;gu_Z+Kk7wlT6dhlSg+dOzM`@-!#UD>@1*L)iPC>OyGe+wm&y=4!B1vmy4?ZHUm zPdE2=e&y|N|CP?ZMssJf^S}Z4Xri*~z5RP{&*!f#gqJ>WX;_d|v2@J#15RQ&H5cY8 zxl5p4px4oWiJ;|i5sfB86pD67j6mJDC+bNwp))}UswR~ofH9g9OMtNHN`VN&oI-0^ zw1e;0qTtT)f6d=R&U*n?e$ny6TD9HM|7Fm;<%^j7HqfTY!?-jQl$C)j+m2*OAa=%p z7O7d-!WC?-GMv{nUv+esi3`!!8kZh+o|%ikmZ0~>>Uvx#7aCqcpLu|FPR7*O5E{#y z%k6p15Gw@Z7;uLfgNi|*=&F8;^7Wt8EKlkpQ2hn-f2X-keZPeNYi>(!NA6NdwaF&P zUO*CJr&>S)KiNkB)DtWY1xS+W3;^OO}7HC+I2M!Cqc52=9>*ahsKXc->f}yP_Uvsb!--agZGkWE= zs$qDle}$9NAW?Ph5d+Bb;=JSQ5ktIAP2H{^u9+|l7`Ht#{ucMB>g1DsF~IeZiEB=} zmd7zdD}_?aD!Qk6vG=PK-9f}({+UND;VEXxykz``m&7c1x?HyMs^`6)W$ciC*!VAVGr$e+ z0hN;-4|^JKTqVsF+iw*=8u2pm>IoQPstww3=HNo{Hj}asFbikuAjX(gjX@VZi<9jS zH36fOBM?1*UOuodjHnz`iKr$1Lg@^^YuJ{sY=VTkLsG?SIK7q@kK`pFy1}q2X+!== z^BYij1r5-5+e%qkS!@S0s_dM-KBz{OGMFPXJYqX8Z#s$&3=CJSMrG6;Fz2t(&KvH$ zaSh>(;ntmN*H!C31=zARCLs4z%z8~b5w^*6VrKV$G=_VL#a>p~%bo(lGJE=#MwOEB zx7V**M>}uckd4vd1*RlvIFM$tNH4+->7b(T zYXwSwqLNcn4P8A^X*MfV<8EGqjyZYj*_PRwSw6bt+GQ=AZM5>_;F~Yny|#UN%anTG z?%UhRY!JEz=BSk{U%WVoSL+oZV1zDO-$38+@!!5z3pm7zm2wrb059&_fb?Dba-M^|nM58v>b z9i`rdQ^PCg-KkF$|7hyu@tqa3e`J2)P?Fzy`LSYEZwD*mOMbHFU&!wLx8K!$vGmiX zUpsW>`pdfGGwWw=JQB`cHEQkZm(eYMtJmBx^M&X?mz{Y1_+|TN#*g)mU%ru_YR})$ z38H^Qg|+eb&dd25fKOj3`_yTthHqwF^)dknrimmPuz)h-OFF>X>6L7_JA;0bIWu^6 zvxFrJ(m)0dUPBYBU4R9IE@q0aXp4U0HT7uj#j=ff~sDo9)!UAwr0tN~^+bGRmxo{byuUcI3 zY>6)vJd6qy6hE)(`6CTKZ(9w!VAbaC8_cb&%#pRV!N$g5lTLS#o5AV>APH3G zvXM*c3;+8gXg}Kj$cvX=icVfiZhqw}$9F&ZBzog3$FF$Ld)_nYe|E2#^Imp$ zX9QBrgHafD&QG_UN3@r|isx>fnVA`EzNe?Bb3CIH!oXT<<)%Ro9L$}N)RIEuNMw$w6oyX7 zB&Cmz>}D?lZPZI?sFw;wIJRU2Wd~YC-6{sHF^fh4e_X?T&=`nJw{BnG-QPXmY7|0U z!^NmrZ*74S7re1OgXd#yR*UCi;@JN)1d*i*jfN~}`07nFwqq(lE9RES(KSw~rdgQo zUt3*Ml)>RD!l9ouF4}N?#*!nehPJlj0$QuR?P#}qbid0~O&I>cy`_R}MwNZSDNPk^ zannhnf8?vSelA7=^ZLV_mdiumeL!z6U<@z|`svXSfAgW=`@qNFd9d9&`|$ek(&OmG zH@yYDuu;8Y`-jG#I(i-(mPrX}kRzE(C27(GP7eGU0`8G^o(vwflLnG9bl@UVdgIkf zNiSF*HLg^GZ~XMk_^Z0Fgkld?)$#YyiRW(9e>u7VciwQdYA7f8e{hXz@QpL*ufVX{ zgU0pSbT>e)@w?AIpMuV70n)lG(|g$ql-018$CRlT^7S=SoZa0VX?2 z;}7YzR2gI!Kv)3wTin()LPMC0YE7?yLHnX#x&$PE8k=+5D%R*HI&-S#ulyeF7PmCa ze}cAlU~tL79s4fOS!@LEXFucAEy1@R7#zQ3Z~*rR)v?MkwQpn!wL$3Nkm~SDs33M# zjFp?g9My==q+Lo3rr#JSj0okuH^NUDpefgg&akFk2j`h`4tKWkSI+TK4mENe`x&p=seEHf0g3=tEAmNf%bv)hKdxHH9O1w zb0$`1$EPmO)Ra&4T&oF%Qx~peodMYN6MI0qNMSTnFa9jzDHLZR^$5B8)r(Q5ehncZ z%QHju6t`l<-L~&|zFM0p74uEE92?g7pUN?qFA*^V2e5A31e88iE0?M+`PSbre;#em zf;qdcVva$7cpd-+FyS$n^aU66p;Zxt>xxl}z!IWL1lF3TSSmJ3F=(JxS#UKhw+M~L zACu_Cb1O*`QAAc&_^GBR$0jxE$!qZ6{v0{=3ZT8$O3Nr!8tJ?9bG}>h-A<%1NxeKU zxGOt9vY6dezUhlsae`8HsGuPfR zdCPwTP8V!Pc-2l|Yk=ZRH>z60dO~rn)L&R~6+*!{FbeOpBGZhl-GKC{f1-!F%D^T~ z&STS^_KAw50_Sl zm+)4ltl(D$T7?CgqqaBlGo4x`KkTjTTpA{xGu^s4!n9i6Gm&v){AG~%mW)LPA__W8 z41h2P(eqC~@W9j9fBL=ee|;KGqW^fM@$=oQR0WodJHVRB`K5 z`Fgdr<#2yC%oD?&n%{rHu>*(qU2i9IOXGryjGE^(azE3a4bWESf74wBvH%9WVvIaHTQln8B8kZI6F*v#XiYVTw#ak6BKc za9Wtlsi@J31})?wf43oRLKHPR?x=xY7=IhD-E`;pv+(_@@sHnh@4feq|MG5Bf9qZ2 z7twdd-@fnH|F1f#z4SS>6Ima;_r814(p%p;{`Otx^~XH^KA;IuSHa;uZ@cHbJeVgtB>DrBNdTrAbw=og(_eq66aBf*wIBRzvi+&r17E-9ny)`l zlg1n7_z~~bldK(WEl%otm@G8l z@l1vtYZXQDLT+7y(V2U~*>eBR+h>(eBg(XZsZ?QH(~Z1lJ?WE19)f>@(W_vvlqqw| zf;hk_lK;3hrwNUT<4(y5+(O|zzkHe8o#x^+K!zs(=-gqO+&#W^zA?UzJb~wk({9ZF zpWni#8uRD0`c60SUm@CpOKty9bm9l&C!DuCKa7lX!c zM-10o&Ig28MN?~eFw3r4F8`oX`5q7_U@?PvW%CcPP|*)ng#*NY2;r@i0Z_w?{RZbs zv6Od8MGbzvt1Ev)_g=v2c_+YRKseM%(s-WYL#00~m;X^E0zpx)X#W9IjLA6k$(G!8 zX^dyW-ZI5%2(UZJ;vPvm?Q&-VKZ6W*MX5JmrbjHNG009LgOaI8>y7|BOpwJI24NzL zW+9Xv2p)jvL_w?vC5v#TQ%xF1x`;i(5A267da7+342*v|u)7$@3}ay`c1%u+VZm|A zHbaERs#Y(Q9aC+$_BE#{*>%hAS6p=9b)oP3*BvlS0+QkyxYJo2t{H}60X%79O0j8> zd>$+)FoQ5L>J8{dsmep%=t%!jOpwNSp-e$!*%cp#aJvxLin_aYAK!cX#g-k%w;sOe zqJy`_OtXIu`ZlQPAk{DbgB%B}5$8I&<=oNS>D;Zkx8#0`6cnNX+KVnjuSUOwei^+H zy#w8c-Y0F%bX?>B3TZTU{IEOh%e;_IYDM#jFE8q+nI8bXqAr{c{3MH!ahZw)=zq|bIZ9x}cBOiS!IUs@f;AEbB| z@;t*prwX<#lLrid5vFOflWA&L0j0+TRWWHY6~s5-1dU@<2J4C;W-|0)PB~#%cSTbJ z(cl?ERW0e5Dj~p33Vq(;UPyvu!SX3f-eFfts%RIcTt(RV?Is7A4ON&}vkc1yiEQeM z%?%VfF@pq0v`f1 z7!>2<#X`|{RUzuWR}y4JHO)LgrDeKa*F3o zQ0zibN}62NFvv|NOlWkMGX>bxG%$izX(0P4Hu6Mf)gU}B5yqHl>9n$J1xB3T=NS~r zXrM~gYr|QC5)m75U@+z@C5smorkSleT-`F-*QNwJR6de%LezYqH=R;6NT`&|Ds;(z zCC{&0y6r1z;%=bdZ0jX z&b^hIi>XYACL%t|O3eT(h!d$JQ%M9u<8lzHh`yg71>^6g$A3W(yw0TAhbQInjZU#i zDb=;<=>^m%&ex1f1to??#lr|GmF2^`ddrHkuyoOd-NdE8xpQt>(H9HbXAWF{ujp&D z7bx~sh!5)P7LN*3Qv+PZ2F3=NmR~xVAvz`+xg0bn&?#usVs%uUCOmNzUiO9ID9i1g z8t9R=J=@nUqZv)Je?{%2w{NYU2y@@s$@Mv4>_0+8kVaFn){Bd}^+Xlu9^XTF_|(!JiC&+9YQ&J>N^D0}RCLa+o<60WoA^aTTTc4P6-{KYyH^ zADjd5`1Y3N0{Q-FxP5y;X4;w8sQ;{LYC+(a>aesHIxgS0l%N3Skg!m@RX9 zYa6CfcvT}Hq$L{GrIw-?Ht1F0cAeA;reTIhyO&;hGcXXXbUGEvH@^klvAnIav3oR_ z-n0j=2b?*bJDj^kqRcc}HtA#L{Tnr2rd86Y87QjF5Mz*?mBYJvOS97*rim)6sMYIc z+4nH$siKg&D5K`IZqS#1=({S>Y62Q6fU6n1JZiZJZT zm$+NUoT!4k&9bcmSRmMRKvELCdm5zF(U>7*fK2jiX-?4DEg(b;CYa%jN(fO@ zCo14k)vE>7QZFeqYwfvivMVX{D$Quh)Pe7TF_24jhDVmtaq~uhXz2tbzUA4TS8!pb zi+d>uLbfxJDTHwl*Gxrto>0JWX=y(K~I4%Vkzfmc`l` zwo@l!8k4k82Hl$}^p)WYpj??A2n-FTWSIIDWgyq<)=+okNp&2!ateZkj1oT>kkmHl zcoH{u291D#tP)Ir5#AXqNKJaV)FR>b&871RnvwUSqVU=O41GS_wd)9OhlI1$|>E;*hDwTyycnacQfS=ib%vYSVN+R zKTAZQnVM#Qf-1Khzm}%>7$PrJVn=>d3>=T@K-0Po@IfFJ85WfxcLH=6F=^rQSd<6l zn8zHHI5DVWVGOXwWI7g_;lwd$F~`MaX4zAOmsdogRH*rytz#ASt7^^EL8HQaz>R<{ zUVJC|9wK0<77Lu(r6_8`gDeingA?jX@;9|Wt~&vLB;qlm5ZwpmX?kF(s|xXAU=Hd3 z5150j7)mkUBP_6~$DpZoKq{Khi<+SsagryB%8b}46dT2|DHcG0F#r&Rs=$ZpoTdeK zkOx?0y0Km!gWr40!p!MYXnpCS3e447S@|9dv@s3 z{SJ6f6XcVhG{gza8CW%x>&Bv?DMB?YWuQ^Zd=NSuAG_$#5meV=^|T8nE+M9>@pv)T zeSHzIG$!}=D^SyRSk#FZdeMO_Jk11 zIe!-Z9>@|iSI#waGcYnakf=81$zVI`tpTW!VX*`VUFPa19U+gTaWIR9q`w?Msa23T z=!fKKNm{S-zjyk9eZ6e0NDxct0t`&p9DU6K3sZ0&$FE&;Xbnd=kI{Q-b4L zfHNp-a-q{AqGnN73{vCLd4ulUDRa$#VXg>n6bztpBD18ZLdo%sS~vDurAC+s)4^~- zp%4``MAI)yKuBbsa!m*B1+clhM#WXM05~u(Jq+N}1+ssa!{=RyI0%K|J2 z#aO^-*)e&|pI_VRcK7#sU-|QEr+U48z22AqVsdcaM>UVXoU7&rxox23 za!9V*2%{tg4*exRT@{qb6;pD5g@@ElQbG>eonDlc41hL9EfCwi0BBTxqbI;p7@nMc z9kEtnD&d2?u5dbvP8V7?YMy@P!0sD~xnKr~Q(oN;F#M^-7tSs|ioaaCvGj_&E+pZ} zQ2OEEi;$`NrqcI#7jY}rgXHzsko?|!tydzmmtKzl`#$mnLi@->Ps<#C$tYKZajd7= z`WworYdi1WxR-b=<_36j9fB*a2H+}N>anNZP=CPK`wFZ$SXW=F;P<^)C(Uq&5 zx}D*u2MQv74N_y3qsw)FtG4aHmhgfrP941PaNoW{+xEoAFGEft@tpBLBh54H$kZz6 z?s4_-?u(A@+|-}*grqT!Tq})m25rXJT=kLGa4JL~E|biKo5#CY2i8BL`RSxWd98 zHbK>D>m)$Y>}xffR|-x{Y){i&n`(E~ruQ}uAAFBp3{({qU3+AoR%cYDthACjo)rVjL8X6+A3}J?U_|gyVM@(PTrSEPx2qfaS=ltk& zM5s)og@Zd7vH^dye`gq6Bpatp&o))V^Z?{=yb^;LT%OQD1^*cAG$ZE$o##*{i~O)9 z(}WssM|1>?O3!WJ_eG7yVxuwsQ}oU83i|lXyMN6O|9$Au@nfjGDUX)O7NE6mZh!9LR2NA}G@BVK74)(|f4!7R?Ez{|QY6!~Xz3gs zHD!W%X8^|mj5NT%Sz!J_XS20rp3OI|+}fygFF$l5+%1>MqRqJ!rI(GK&UEXP_>dOAt)V%JA?MLfA z(~j-D0;Z)YN!0P*w30jSyCZ4gz1w%l@6!OY&@-5z*wF6t;`pWdY#shy76%3S5$Be2 zJ0yptUgJqBa@yx%rbAJh5h|AvMAEDP{Tc?@s&SJHFc@5Yc{Kif+OJ>RdvqPJ-$G)D zc+c(olGQ2H+_qPKuHrZ4cP*L2+08rBdygL&KaN_>+kflV_M$mYZ7$K3t^01@jovo7 zYunx($wVeCkgZ^UzFE#0v~)OGWGr8kfH0Q=?pTu0SbzwijBs!`q#aB%I+6 z)I%>E-Lq?YW@dWVo`*LNio|Zt6)FR-rPsZRW zx0`_R8#Hy*@rIplEdkpnC-bskoOYAf0sG?yc4r{Q*KV5f=N(q+=lI6aHS4>F#GKa(LdCVzA{H_FHY zC^lNh0QK;j5D45lm`hZNTr912cu`l=CL6 z1DncK;E2VF116gW3PtRp#DAuWkp>CW9d4fD5V5(L2y`!KcwxzuIgFh=29v`xLa^<} z4_g#zG6eu$rfGAg8hIUBfvEIdGfY6^BcyGK+_jqRS}Fr^flNRWh!Pgry4wi7C~x@` z8Xw5Cv1QFQ?op-3OrZnW7e$|wD{`gb!W305P6i7}2`W|9B8aI9BY(kCB4Ypu3^K)B zhEmvsL1q|n4GCn}V3W08hp@>DN)?`iaA*sQ$OT#EQK*ax=_Df5HXLgz*3>#ag?$-= z7D0&$0qtjm97EUN%DE-eQpG>tvXq246 zw{abYy#{UB2F;@-3(kH5jkpTe0Yt7{4)9RY3#JPg$rNX$WE(C*|BB?Ib|c8=!+fEYczsY?m_gA^^udZITN0bCnRQsrbgbrNL-n+?oQ`G!O3#tuQ&c17ya z%X|m$gibGz?iK~$ z1MrCyH6v)qI*OpJ$zT-)(2uqhZg4v}2+X+U*y}D|D71fTMiGdcnH<f_%>_(HfLHs$(YEC%-zg=?gW&{y|dh<80;8w^<%J@1poV65Ni zN~?eAJztl>P`uKM&Crv9XveBnrO1$@ADZ!N#Q-}4LavLA&?9v7D)9p&?ydkGfCvUl z#MT2f@jO>7Ev&{dH*AHQd94j66vuDrdDF0U7RRfnnyqA!FRF>^HBSxVt_kLI%Xbvv zdTlLla%ZVPRu`7i>w#{cw@ncKXR!KWi7bCTKh5arFCYuEbH_~!bzc7CxyH)IN+bP1 z?eoX?zwGqu8knq=H$j`LYx!f!KMM7n+BMQ9UpcnPXU?e@IC_b|oU;p5z zAARI_Owa@4+aG)oh0cq=anHGSgDOe$SONHrRNPRpi@t;M$a(3TC_nz8eB(I`5>H4u^leH5tqR@tld}Q5)~0`-h9f^)IW&v}|g|&iRU8 z$cKTVRpQLB{&m6oUHXS$%^^TDp9U556U8fZ^w zrLZE`WacvSm~Z*Pv90ic*>dSjKlT+Ki{hD*v(o_>*cvu=>}g1B6`KL4>C#^(yC#7+Nujxf>Fb+eZMnh`lwcaN^?RdC)MV>OqE5JJZO8;PNxFZdHJl2miJ6|J zBS>fcxv*R4wG+AGG~4q@h9$a#Q9pfDw3(kEg2^&UnIA3l*Cs*`to<}s%|^}1wr)VL z0Nugv?D|0sRdF}mbdP1;0X&pxb*Dr{D3Ru0?3+~}699G+v4hf~##T&~hF=sRmm0|o zgl2F@Ce=U@_;}7#<>G%w!;a)iR-tLLOT{VACSaaB+yppPu3_pCEGn((Vi^?VZ%Tlv zs&T#q*b1l@e(HuZFH?eSAOND~U6sTl&rHyRAZ)?d_x9x%YjS@Z`9KvKAp{j>nai9S zxr-NJ0Pi1C66NY%K#(U}#sK95bmjF66#|M(AxcB7d50GbsF;7vFIP=CtEpxT%9>dN z%iZUO$3kKlLU#>N<$36RT;z31N{b5tR45mA5W}oeszD=G0HG^P#N}zfg)WnNLyf9i zrQ(#sW{_&WFtCfDV-zK<$>0f*>BJs_^hysD+Qz;>9{5 zh77y_hl-f5IU9d=={lLTaq67E>Nx~d3KDA%dIea|*Pu6`x1!%gA4b2A9z}nQK7;-g z{TX@|{T2FK^k30)=%3J!0b9P5+Am-=U<-SIXd_&}F|Ohop298M#HTZgbGrk4C9={Qv#c#sD4mLK~oAEvP?fAFwz4$%&0sJ6- z2tSPfE&eF}7=8?Y0)GxP8uRyb2Elv@z_s=_Yt#;AR`H4cuV_r=ex8 zlmG))GSx8Zf#i_w@Ak=1RxCGtYr{hU{BAy_1@PESUDrGqw6jVhI5$YoL6KybJy@0p z!yl>%(`rO=t`qHqgnvA6{RAE^Z%k&=(%lfrbKQO<2UOkzH3IaBDsqz_G3dw+wnrct zqG2s_Ms?E)L4vRz6ptHmUNm4L!)46Aa7af zB|S0uksf#9Bdsz$lK1XFOFwf?RuYIrhQbCml4r7G(Oph@>&Rag@MO6jVYCeI!4g~w z*3puHU+^Wbp8JAQPy^i7nVjhG^c(xjgJ86r7C2WLqZe@M+DI(})e|`N-6PZlfsqiZ z2%*w6AE8Q!3N2UWFfu(=90ak&ZvlpEf0(kTChn2R3*+YrSPvN)aoAgt}bbWJ<&K zM?V6ym5zVF=^30>csKm|k7z|!KmK!y|AgNHR>(nd1xBw2L-#%?!N3CAfJc3nQE1=7 zN@Kina5osxZ1+Bb9>dkMx5=j=v_a+oc9yX<; zTV;YJR47%Al5R&LrRZ^j=H+%o<7ZABI|_GQd{`Pxhve=1Da;Jn3qLPdlfQrKPPuhV zww?o_Uy>g(7oU(IFLg~iS(A$>EjNUNC3p{MH@i*B@pBa*dKiztpIY;20wn*eip7Ia z1g^h-{sd|AMVyw>gbg>+nk;DKbr|Z9Nx|PP(+LGld}OlEp@6b#3c6U0G~qm3l}X=t zFCqJ2MA#Zj8-kvK2@&I?SeQEH=vtXvDa80ou_33&gULdN9{}}6{)UY$shd|>nzuxU&R$encPQ*Z|qRX@uK|D2rW2l)F$q`noQ91a>wlX(Cg96v1NbH;C~1HmZ( z|L+ERKp}Y$Ag81t+=7vLH&pg_H~k95&!t|DGA|REmh%**>#2w41$g57lj%Ske-Gg9 zKqi$s@}~4o=^`X@XADqV838PDITybMN0-9b;qTVRUq2xy=J>IrDG{K9`_jouKLIbn zu&q5UkN%QEi!?Vd~}&(b&1GyBi(Kd`p*qJ4KHQ~S?$8cPei zmKM)87V$#TSXh2y|JirHDQRQ0f5-a$XZNEoEp4O)ns9CB!sc2N8RM6`e=-QS=SIB| ze0Bs{zvix+58Zs%HJ>16JT}nqjyuMGfnJE8n%ItGQcPE4yE5<&w@pGK_@38a@rm(I zUuTTU-Jl&Ul4$%FSA7yWXuI0Z*JG=(X?wnx97$IIa&c}$m*!$-_zgRQY_}_!vYVxi z$;CQg`=J*NziE1|qZn32e}fM8!ULYw11#!nv^qxQDwerJXDGT{SY)eamyCQNx^#Lz z^t=mp1vk9$%qar$SzQkTT*xb?x_q&g=hqxp)$?oP=JB(+Fjve~bF*p0Wpg9Fp4Eh>x^pQ<)fG1VA%%1}&5a3>+(I_}ov8aub@=_W8e}3n89$j3VTU>JUDeJ0kZp>|~28!t?&Nbbrxw;{!RQYuOG}QPA$6&xTH2bnQtf zq%+A6ly^Kwm@W6`f6{DTx7EDnI6G%z!mA6tj+&^pV+5+E$}D)(3tFI&xTSm5MkzVA zbx>>h)8S}oJC*zNYM}}W>LrsfB#Z!Lnuu-1;RXv^G3U*uQd2C3^-xn727RQ~Wz;2! zZB=0^BWAm3sg9Z#z8kK2x=9%mrStP7fKR4!o!n|}U+$Nre^n=A8xkhWU@E7+}Q$&l&NOrE&*vJa`snctVhBxc)|cz$o)id z+*`fy`44asn&fMqB4R2KD&BhQEEv_j-Hf&Now{rrW4n8?>b2I}?ezoet=78TQcXi^ z$#ojjimofue{cXYX&%Js{e+*QoG*s?TVDD*Fc&B@^*kr<$`zJG1%^}4Ih3DpoYx~llYB)9Q=(%)!(bc$fpn$a-^Qn90`Bb-t))B*7$y1nOAgt{ALZl4Rq|5$?q+?0AfI$zgQ6IZS$%zq2KHAJ(Ji)A%EH< z=(d->H9d3ekuM%U_6Vq|_Vmo$^zj$RAD@}Q_q;ea^U{}Yzx|O%=DyIq`F8mC$lTHM zJhS=CFxO0djz9mI>=!@s%g2v>n4sqW_Y*T2|F`(d>3*a#<3Kw0IwbNs!)zOI;eoDb zn||XJ&A^(w;+89B)F56ccCtJq{mqOBFvI*LO=Cz?*{fWCeTS}OEXS&kS7;OsLUU#s72Cb3Ywl-Umm^!6-lz;B#n5J$H zrK>0D4T3%>jiL13z@wvfr=O%suFWU@o$R?lqdZN_+1yM+dV|P3r6o08x-|}rL~f$* zC@=umtDY7V&3_;d;?1 z=plFx#ah`gRX|XlDnqGA(PTh0HcNHWP%JGkK(4=pJ{+5I0QL<)Y;#%|_X#8o28OB0 z;2)P;fWup^q}I8kfa=j*ka8+OSvxLg8QKzDYO+5jR#{$?fKSC0z<gn zh>RDfpj^7RfPW03u**rwm4QR5Ar*Vj+*J+BR2Q8{ed@gsQ3eJB-iC5gc>--ykc6?o9e!hY3Z_MMl@w*#Kfa(@cKK&F)|Ni=^$v>ja zH^w)oCGTs@kMqkrmzOt|o*fJh!(X@i-9b2r;Vqk(`AY922LSo>b31Z-a#!T8&6CPV zJW07jZbj2{L)FUpLvA&8Y}2T@_#?WeRop{PJ+_^=>K@9MHPv|Ocgn5ywpP2EeW8DM zJ#OtA?0D1FZ&{5lZa#F~g`-kboB6GiPf0r{G`-sHtakM6d-rU6qi64)zuF4^_-I*E zmZNg-oDB6r?wo?Lxh;3Mld?%x0RWQ;N-lr?fA#e)aE@K&ouJM+_nvd#_exjyUP+}Y zm86nX?}t>9x?SDXcDLJZyI9^C zNpJjF8oNTLorjcfvEeKj0-HcRoyS6lY2`ji#Jp$7_cMvHfSv5Nb{fK#B?(Gw5Y61{ z^l_Qc6T!iEyP7UEQS6Q{;ZWij88jc{at12Xk0lW!^$_nPA?JKQvh|L?* zSbw=y-#)*3Q}gu7fuB5VPer=iwHj4>*erq>BGwZtwOXCaWvhl%>S;gVDe`2FWu|v) zvVIEmrIy~B!?s}Bp_569bGy0cZ`+oW&y-r(A#ODN!`1FO=gWU=_5goqD1~1sl_G9r zsw8U}7meET3bA&(;ghvOjS20*F}sr=cuB2VU65u`Fz@1*rTtA)@OvDSt&p%Vww9I; zpIQmis^V2h55sYb0ZA^eRZAKQoBu76QD&-;*2dt$qcnDRaqfq1E1T*xtNfY5dkP;b z{8om*3wwo4zUDB z5)EQXW&j>5Qa4x}?mB|`jmr$PSFCu|W1B;t4sA4oNa@gO8VP@rc*9)uAKLzDq8O9(%j;%Z306KvDG}b zLx(i1&F!l!&jf$^gKL`e8!vT2lbOyJmX=poU) zL_ZkX@obez&1Co%GLht7-UH0SWXcBzMbKm{52#QRT<;&<&)*P-Nw%0dcDYiEOMzu2 z8`~Shh0adzjVYU37}c_><&;b9=H-`#Niv@#B%BcP`h(bVvSykV;}g9;TWSo~w>Qc` zsWcb5mp6agrLtpHv)Zsxz4`0dBg)9r5l6ivW95XIHFKYGG?cJk? z&kt56f7rffZO1jDXtBAv)N0iKr}BK6{x8D*ROZ)Dnnfo{(?)hOT^^jf?C9?HYQ4&R zEOkyk64V>5rPbzQ6dCT$+C8uRp<8D%rYFjzQ8|Bz2k8yX#AnT6^Xt$34ZCmhbNuAw zx7bF?6(da zF29>a;*W%>hX1EoEXU?WDSw8PUMbnDiy?dTCT=VC|5{8=Zjl}eLHF@I8Uw+dgkNn>6u?x4~K6y1A z>>oSEs(WnZ2s^g-)RFzYy*)PE`}`h?vZlTxz_nZ}oGP3v+@@oH-*~VJC|kMX>!qmb zYz8R~{|gg{F4~K%o#;2>b9n0_zfCqyM`M54gLF{i7U0OWM?XA7fX6?^iLQAN_BH#$ zzd%#HJioZ|lI6wua?y8vH&hNcipj?QYwtL+TZs*$RV+<@J}xiUT=q|DE1k-GXRg+X zi%W6+Oy{z_&Y8ONXCCRW+3l^d>m5Ja@1H&H`B$IcvX7YMIC5)~-)yWk=>Jps2S9%s z!l2~%zEcW9Kdw}lT5FxlHV+R@9y`20I=XZ8#0@KRdw~-)JCkoOFEuYOl}4jte0ej9 zS2pHiZ*R|w=PJ^294lQ~UhMR|y|a7%@=~`~JpQ85e|Caar}@UT(O6$tNb`>=&f{)vXlVcQRm)0W99lgw`G2-FLs8S7#p9}-mIDj7n{~$KB&-;0t zF*wo8c*h%_(CPNG0|ROMFuU|)_b5xs!|h}^;(vVFwTf}kb{b3C>K!0bh< zSv+%wTX5QoHf4@vsTGe=6qw>cav)3`pX*#wv=6kT|xNg7#fXi@=?a>rD}Dl zR{L_dRI4pjt98ad^+nT{(l>vDy;j>Nb6C;;k{?H{y>}XRJ*5YmGTIvZ=TR3H5a07; z`&u&2sfd&m>tacI#5@BsM_qnC7;XLj?{ne7--nwab}tO0xHW1REL@g6wM~*@tD!eP z$2ymXW&psC@bV6;)%|FRloVx>Qtp|?nQ+9|ov7+km6U(|S;IaFw-|q8m)2nl;=w{m zub3C&L4(0@uN!heDGz%Z$+$?bzy3bEgZ=K~T)dKf|MRbU)#oo=81nWr#=M-vC-Wz0w+dOJfo~XV&6R0?Ai78IFw@H>2 z8_|*7TFE65ztl|c3(8UGxWT>q`K4rF<(Kk1|9h8mqt`s-bQ{(Em2FEza9mz)+rDso z>ASY;-_u+YpL4>f6!7pHz2;&2M=vEsG%)q0_by(#l)B5;^5cIZ{~qzxt-|5L8JgX< zQ_sD*@J`Yuf3@(h3ZKoRkADaRf1WjSvnS8;2=u(+7!VdT1IL(-z5F}%%rHTQ%&a`v z>7)OK8vRx(?R}@0q;`A5ZoAh`aU6h3X#=b4qux+^>|q~&x4XN&#`NRJ+8Rpz=j_0pE0Tm`!Y8dxNq_Fe-J>;O@QdGp z<4ePGtoau;JmjlsSn|7372l%3G|W%QRK|7?NHcz27GNi~C4d-W2qQMRqqgWgvX6zr zxgZ3>BRzvg#WqW(JWTo-)_Ht>)KFT1ZNzX()6vpodcA*dF00tiLTkgXl}DSyV~6Td zyEva!`~6P!#cPpOrkmL08LNpEus$;%@}#UwD!uwygf0l0mRywB4OE*Lk?|_|x^0UR zS$wh27LCZHOQ|YPQcYu+?Vhl|!mvmt>4@zAy#`Z2eY+gH zsY+=TBhY_yYfD5Vd+#FM@PWdk#Qncmc&706!Z!>5SK+@FzE}7Gc2bf{urQibHu-qw z$(B3A~+wu@d375nwjvD)22^O$@D05<`J5&;p&oR%esG;bO{np*!iI^aa)s|ZkeaPRZEMXF-e8+Z6ZYxE*kEzUiHaXw}r3+&mo&b#_x^o`k_XeM%9qr=fyOhvFBBr zL9KsU>?V_s_3LDkmlmsjn$#9ejk$7IC~g~RP_2YD6*cU0lcyMhK%~*AloKzrN{dn~ z{k&hz%6%JA5v;r#!$n;wM^%aN{nMvpRjTOqhE=o>^w2qEO#U;yslTD)zO1kZgn9Jn zGB;hP5~`}_nx5U=Kk4~iHQ7msHa&4N?bLsWkw$KrE2A4Fw%Kd+owBd&s^vGAn!e=| zeKPL%>xReYcN{?-QuAFmsFow_kX8DrcdD2bdr{F<57+xb^c&I-?c!<_mh6tHIIDIO zr)b%6J+6j}Ce6G?v7SW4JtMm!)2{9O2kF-RtelmU4D7P1cIC-psi?x8v=)&CD3pI` zOVY;J(5hRi;{TvPpKiRNf_ibNjvRN&uRL!1^8>%*lR7es)fXEi$bd+-1Z%6<1kyc% zMaQ}BtLU}HdwNF%&Y$XWe(%KxpV^F)B>u(U{n^*O?*1<{uWE1Y-?hEFCN?fU_?>Iw zcHHl8?68ggi4)&mUmm9idj8X-|DJyY5_u<|P*Y#yeE*`Wvk2unQGaTm4Rsu+Mi-j$ zZ({qb+1CMd>;O0wQ*j8Xn80C;KX!rG0d->L6oKIB+eNZoE51SUpEM*Nc$7lvv>9Y+ z6T@s~wKXsHWlDA{BOylACMI7|e!E=t%1N`_o0CUg_H{d4k1l?UEUciiaP5Ejc`}$S z1s4gbSFKqLT^~;sHXh^>U66e!=ViU#I2J7?b|)?=i>x}t>Q?vXyJ6Xu!km-hv^Uvb zzP@qEFMYx%d9)+)Vdhvf+95CiUWQ&K)rcqgR<4c`82wPLV!nPKRqY5(ITsjgcJX+z z^+1d4Z(@q`Jp?-uCyh(l!rFh@&X8F9M1&(^>Q#C@{7obq;V*0BqgMW z3>F0BZ&HSY%%cne`B0{_b#@R0iJkg<)H%{mQR9hak^&=>1&#noGU0#a009vi*OK%e z4QQErO}2F~aQ?leUS`=1 zy6*t=YDkB>yL16^{tiZPImQywJ|7RTkwt}$_TYsNghuR#4ajO3lJ@Iz*xn4Q@gNIYFO>1VSzdH#Q4A2 zA4oz1u8PR$X?Virk(~-#AYR)r#apHdpY-VyF)y>NrmkI^amU&@`(l zRGvDV%xr1cv8jLk*bK9U%{pSdu^?^6R?uuNxKoVa|YR4-du#-sH#yog! zBVl;pnl60J>9W{VlYja5vAqz4wi8m6W}hhhU$5PSV8ol+T^2mynooQxbolGU|%S`)zna@~Y37&G_-# z=+`Mv0iKom@0@r%hmd6N%26dJ&sz9!-|G}z$M(OXaMIy_9mBm&P5S~(pZb(hn}{{i zSBF@sFLjPO!zCS)1iB|oOM4k2&$6qyg|I?6zl#*>q>_XkHWN)&cMKW_`bp*^bw-s0 zq;DguhEab_gr+eHHlM2lFHJ90~bM0r;Fpd3I;s( zW1FNg-Bf=eMNlQsx)ey4s1Jirpr}%%u#=cmC1jSLlS(MiG5}+_;j5)dUI(wKcHMEazBPsp*xi~-{K!B z+?MAzC0>C2y#848n=-PZ@3H=LrDAF~>df2Pj%sV(qAe7q<3z_9`| z2+QmFIs%R`{{P-FY6ne0Kp{_1H}7IEK79v#qo7 z)DT#hAyLu4>89Bf18!!4YF9OqX!;(w;0&in^M~+3sWUEx?r@1rXBpR>*t653#W8=3 z)rG2^C@)+&b=%2()sRayuRQLaTLWSn)A5^&J8O-lZOd9(*tqcGt*Y&lqFD9z$TolX z{H!BoR@Rr+OhXx^)uvNcmS+PChC6c$)i7F5msXFpDl~aW zN$6g_SScx^;tP#$+jOn4c-dS#A#OCRHT`P5?-qY?yp<`%0Fuw{+h4YiF)`RS?x` zKXv|!pTDQ2N?$4_r6K~dKfX>E>;(b)YLKawsC4!9aij63^Vgj{eRsK7Idj-Zn#D@i zjY@`bu!h4wL^|nQVOJx=K!rG1B^ZtdmlokhS&|>m7y5>y!7QZJ7>H1)Hk=E>l?Ww#{!w+RuLmMe%S$hQg4FqNqsVk+vxZ<8)^RMS{d>2E{$ zjZ=~&Qr_`482qL(EDc-oHf(=1Ikroe9<12|AE#e92E=vv^J4xl@GJ*FZ3CrRhnI$Y zHtBn<-X$=$S_C5OV#J2YqeMq0Yj(+OnHeK+RR`nRJ zKUQt+9rNwj_EUNt^Dp_lnL-kkLt_t+5lrV^%Tz-BppzQW=b!Syq_elj?wmaQ+;ePe z@)aSGU+vUZe{1>l%Bhu0Yn?@!Ckurg*d}oiOA$KGNV^3Tb3cDCNdb5YeoL*=*1NAh^Y3LWkJQ%K zy1rw%SQ0@muqO8~gSFDopkIRB>{ZvwWYKii%7<1={O=f#J+R9Xo;W+d)-)T?cO=qTCqtL zD>p3?{$X|fv}1EOdJtgUacSk8t+*5Za!haRJ{etoq`opZv@{MgR!v?uUTkKa#yPGB5i(h}qPL*YM zPrlXtIsV0sQJ>Attv|HG=V}`tvgmTp`zbsB$bYfhU-Z&j{K>1>!yAM1H{BZ0xA$&r z6)y4NcaUtneTKC(1yi0a?+v9+(=apk`#K(%gw$?VGrVJ5A75ZE$n^@M-=)!?qGnza zXT5*jE*TwotEEo?ZQK8PyTnHO@5U&sTj?G9RqZy3P0 zUZ;4F!nf=7Zln7%KXsy)x~|>sthX0V9(v^X!e0~5cfZwa_gdGCM{^Y` zv9n)3a@yv6(J(uL&5_kpDq4=mBldpC78$BCG9#mZv%vnk1K(lpnKc@Ew_1{Q=~Ru#ly#qU*aP3Q7l|`3$Sav8 zy^c!8_-wGG$KLVu)9jC)e){6kr=NcQ>8JVAKmI3o;>FI;+ehhjNymS9Ie(le%PzDE zrwTs_IXmd|R0iLAQk&qhi*4;eudN38blYQbesG@AX~$e(@98)&tWZ#MJM0I9$DM+PENe_2UbY<+D1oy$LYS_k@Aur~85?lo#C=iSCMXZxoo~ibJ*&Mz^y^ zmrtG)tvc!$JD=_xQevw^7WU*DZ9n?h3ig!-E+igLHuIPCHA~>tO*n- zWg8-DG&-@j=$#@lEd0&3^e2yW51Zn$4R+h|AvO8iPshH^Zt)z)J9Lg%m5~x@HMUYX zZ3-7@fRc^1L8X7h<2#kG{TH+k(H*C;qwvE4DtZiQSdp-KpKJf=O|iSr^^(I-?_eRH zGn&ga+z3S2g#D+mTn;B?dS}bOcXE7F*jee!M^334xJBPmZsb%_O9nxyRPM}0^9!jH z+OA2`Z7CG~^v)1ZnLM+-{pWxFjHRsdLRPGhk!{+x=hJ`WcZEp}=XQA^o$LCJ>H5l7 zh7stk!>6@A`IuNBZ8|K}$lll~94}l)_QtIwuU}udzwo}ohYFu9{BMPC75)n|S&NO> z3HB29O7@fNt?Zw(kFd|Mr`R*>YwYj1;ye5bew)S{csG9!{{a6j{uuv#{_prR{A>Jg z`1klll5Bq!F)v!8Cr09kI4(|!GvWsEGvZz1J>p-8N5rp*kBE-E-KthZb5w|?FF4eQg^6V{ikKe7Iu^)>4`>pxn5Z+*}Dfos)&9EuP5bZc|J(it z$H*}jciKY*SBa$EQahu8)_c1O;VPFhL5D!{8fnZ^OebV5)t;4anfbguw@hx1J4TPN)nof&DU(rvQURaucDkG+&ucUL87 zOFzY(@ewLa?5C?>)n(J?^;*CwQ@9azBW8co zrK1QX%o4aGw?{ps2Op{J-2pmA=^&!CH%jvb5b8Qa_dw&yfX_J1PB-f6dxq2AqbBk} zD$8&qhkI?(1A0b}ctQRYDieTf<7{_K&BBUV$a_?e;aJ-@Gq_41!fcVw+}2BuqapAx z=e0@)986-b8p-u2{-d+?JKW>8cgBC%TEi2OR%5&iH7`jOFmi~yA`YpgaVn9jG!Qqq8nG2fHaFDlbRB0YnJhzkQUB zwq;ve%o61BqH67o2W17~)){}_=%%a< z0Q@oW)iD55dpk*+YBKC1vAd$0j2dLzW)fK~hofGqtG7EGCvEEAq?{nImww}WhNzA{ zh%bOUMJw-YECA(}C*40Dj7TA)>J754fus%?Lk5V zN);Pnf}z~rHc7WJ&C52@nP`6(#vFzeAz{ccHtLeRpq8s15}je1l&P!8X75o??~+!Z zFRBi5br6$Ur?Cs5^jY+$Q;nI{d)~UyOG#W4f2B0e) zT+#(aW(nx1N8K>iLqxas6d*%&Gmo$Vm61``_mbV79w_uNJjJnxM$>(vWJc#U3M%-DcN_#jgsjwjk@DJYYtRkr9q5)-5t=oUVkLA zBVx()a=VV-BYS5aBL}j;Lc9+6*kni`1KmMm?WdT@-62L30XAbDbFayC4D9_eNns4i zXn=Wuunugw5@E`AcbI>X7J_o=X3W+0VYZ*+%x>YyEh4;L1`=r?q&O>MGjfzSf4r0L z_o7=y!*ri@0klFxh>@eG2CO@3FYu$#N45G#9kmPY4b}xJ!C=4@V7S|-iHTw|s*=)s z;bcHf{COpO%Xb54d{CI~79nzy|)cVAVBqobK zIpxGJG$^;mBzA~`66)xzRfebnQvg$< zy6-}QCAp=o5Z#va+9`g}z$U~t#>tfZc9C6vs7*HQM@#p2bzUR1k(g+27jQEpAoS|C zl~GgTV{HXyL8DA^cNhL~UBr@Q+u5Miz)m)u>norTXd-`BEzPgnJp?NeV;IYHgpnU> zwYl9R(!>l>^t>_%Cu;+usSo)|yn@PTk5Wn9vI|NjiKAsp-YvvDXn-?}HSyRo`jXfK zAgCLVaoubHQ8-pKK@w6nbkZd@EX9D>IV*Ysri{|6O*Ubf(eYfV00SxTz~X3lE0*+^aqj(#0T*#>e>E~ z=nLYl4ZJ5vnw=5Rh}_EEKxrDZNtn`r5rODse&mbAGpI3Lte3y( zG=S!`Fe^Y<;IP{}@c9@LX#jbU=oW;X$XUZAy%0o0fOs<8dKNvg=w3Xh17mnhkP|QgI{8pu3?(D z0lRt;rlTuvWx;#MCfT+HJbRi-4np zd`rkjY~?sBc#6=+EbjPpqRut&GL_2=z~CTeD&VY0CHcT_p-T*x10Go9@NgV?BJm6z zO+z;VWXXbUCJbK10t8cv^F;5jYZw=S3}t_rMderqn+ME{hKt1xr~!nY903Tx(h1MA zW18@|xSek0q%j)6T6R%&!@d@qrc7!}(9YB52y{oPqaNv$lLH|27%%5IKnQBQ442Ul zmmY)%2#qi$MX;?=Y?x{2qi_|FdEM|7#}V33AiqMQ`^5C5m|`j6Iohj(>z@W^|5ktB zZ|c*AuFIp0J;Ht)St@iJ)eP}Q7!hL+(4|IHQkHjEySM89`dOc6D_p#vzP=2RPc-rx z7)O{cH5c$pBgWDqozM*|qiTlna>l;E*!vl$`?L#-PAZAUP^q6s#pvxLtb;Ds_&@qK zy=d*V_d`(KU(aP7orX zkK-uqOQ?Ge$CYCRe_Hnv2lWb;_cPocFieQ=mMG{U#@>}T8&ss9sVSeDb&|du<$6cm z)YTWJcpvAp!P8jYLKjDGREy6t(m-`6I@3Y%`oo-#Cx(|=#Qv1-{K~X6&i;Q0-GPBs z4jn+aKigvbR~UPrK94eWk5O$Tn^q10;yQ{J+5LbGI<1nW`vhKa4k#esI@cWmKO++u z%9y1_pDi-xz|%mO)83~_JIMTG<5FcdhlYr4? zxR7ah(>bMQtd-GsTk_vPhogVeUuNGVMx}88mNjK#+(4tBMGKWiV&aeK#?euOy7HY! zYlJo%mXWssH(_vh7<(EdM9=-F^s_RZF9tPA_c+K{1DzSBXWln)Du8b7dh87F#@_U;lS>h?lq9iUWr{yUeW|}<#%?D0U}C&qm!o`1?y}c2)BHuUt%P@s9J6I z9xBAPNebE6gHaU$^m{`n+ko#I(YvkT{k@o^3gGz7MDSLdxkP`5o^tF*;iUA7)DlB+ z$2QCi$$pi1LeEpqd=FEM+VqeOLGeqBH8Cq*3cqbV-#QxKj;(4iGO88`k8asUB~f>J zmbn9I#{B?&tqgSx<7Fyr5>i)+H33;I&4fT&_f6+=P|;TAW-WFj%1`(a%l@`E6rX*kvn`DEEG&xI7h@Io;hjc5iwAo*xi^aWHc6mW*(&p9@hm6LXHDuM@}*j0X5#9FWRM$ zDn&DTutFrNDM5N(Tn6uT6VHzHTr_O%&@&9p2^25Rt3`jzV884J!oy5(gjF=cl1)>V zE@{*%o?+$WuKX}CV&tX3bX8n=+!LX%kiD6Rn5YPZENtC3E;v1PuSflFml;uG053ul zlb5lacY;6IAbG)-iP(*LkQKq+gzchzI_APs031<~7HAqaL_$(1$ zjxeMG7h``O5)z;`te6FL!*CRhfm5N)UQi`Mazcq{ zwSY)YV1_9haNk#VJ9MW9KR@2m+>LGy!>dVj7< zs&RpD3(Hv&rq#F*)+kTez~r2)T3&547un98D1&|Ks|ON5^+GKNS}VJ%%}Es#t} z?7)Adj^sK|k>w%iYSCzJ!6psG6T2iXtAhu zGei@Sd=x5x+r7oq0x*Ly!=d4UOJER`WhmR^j!vS&q%KEmeE4rUbT8aELlVfs2t4Fy z7raKj5(j|jCmNSw;((G2p9s@)Ep(snXsCZ1YEoG@%i>E^f@#AU%EhFzkpvietKrem zi;@T>;FZx0M5UJ08X`1N6Az+!MU&E!hVqFdD=u*lCIaZm$agAp7N0XUn+C~?ts)h` z-6{==kC1yB6W|OPR2kPQlT@Rb9(!;_M1BpS_tu%?6X`NKf&?f^i!#kP%G7tnNKAh% zzW}pis$3${U?ST(@eG}uMN2U~G7@|0o~OA=u2o}{Ee;200cYJJeo3IWN@nZ9urU2C zp9WP0GPff*a9+V+X;K(skt>UOlj{r=(43)gXqRTV2DlIdmX_|saRIeW4(dFHBg%#q-rYH)cSusO&n9?_0_sFh)+Y$Qg;}#q;U{XFbGac$09yz z!)n4TM=E+t=Yw$|x7{>^w&WAJ08>SH#M(_GL^26;ZmvjFV`|evkqTlkJ2&D&%xdIq zr@BRSF{TulwSufcB2PiX=>Y(i(cFOND8fug7BM}`rJmO`1F5$HWf4^nyH|funy8AV z_T<(eQFcrX&k~eM2k228S$sUw3Vo@~82AJ+Gz7eeYb$}nE`rCq&Qz*xLK8h`YPQbe z5+Ls_lGc_+pCS6BGJ}w~3VIZh5(+Ld4N<>(Ceqn+@Ohe}9+p&n}L{`hwvW6@wTj!g<^{|xSyUp$@YH_AO4yfkpK{ui0kufTlIRoxgC_9u#%PnDNU<1-l_Dw`eJ9e z2opJ;vjXA8F#ezNdR?7tlVwuEfzh?IxbFK`6XXpfuzf%n}(Xyrir3c`tc z7VCu$C?5bLj7RxKxlXp7yI5;HEqL$H5u`ZNX)UIZF$u3Ncf)@<5+Xz@o-8eARy6Mm zAU|_8*RF9VDm9A$Rpb6zcdOHV^J-A*uAQ%XQEA0I+UX{dT5?OJauOz{Tu%*46_rCw z+vxxZ>McaZX33p1MG{1|ccU~(KE>6*4FXFm{ro0d`IRJdT>X1*u(* zeb4rg3)l?2-ClpwFoUi8wys}TrV8DBx)nP{Wqrqsosdk%AU?gZU>iy~rEa6`kOa0* zg%;f|?6j4JWxtr+?sx&3wHCW2NxFbf8kp_O{Cz?Z6ym}H5OrtHkGdzdN4qEvj8w&aVhB&mYU;w(7N;E^CvjeABO9IDPs8yY0y*?`J3I-}hMgt#6%tlD+8S zSNPTs;UL+CN?}pQAQqd&wodRW2H6<@RU3=KZTjBswdu312G8>TGs~O3!`(Xmx?9b< z={wE;<)s0&Zl-GFv3l2veX7A$;ToEyZ!SDk_;}%qh5t}^9)5;XSHT&q z;{bnibd#wm6bAgYu%S2eu`*k24b-LpB9aYqoC}atH*3xO$oqPSAvwUC z9Hc~nIvcyG0jrYSXAJM7X(r!kq;#(;Bs5IIJ6LkyKq~xuM%tn~O;hYlA@EmH8yoLx&F5bvzd} zDacfVtroM;rKg+iu=Zn+tXvLj8%2Mup(*aOW9>@XUxdAATgnJTF$$>EQ>C!Vt}zW? zlt?8o{Z-fUh(W}p?3f~j?o;x&t;1bnl%*ZdNlfd-p;JEE-S1VPO7`bA=cx-M4_)Ej z%EbP1)xi=UFGtWSyeVM5p)o59p65?)u4ZvKxOGz45QM zzsu&n+x{v`cP4+%7bkzQ_5DkJ?;E05*eU#M;R$$|A~giu|J%Vd2w$$T;sPLpQ;n32 zos~W!a*J8t)MBjMCgB7prfGa8Hct_=idd-@^5h_jS9{UZT}b7mlVe~TXdA&DynYqwx*A({ zT4($@pw6hjiBBg z|JGxlhDGq1$Nmj8B>JBJ{&fEPF~+|^6#4e+obXVNl1B=>oP;{jje~#eGDEEe+k)LC z*5Bk+5vQcJE@#>C`o8eREh5rr{I?Iy)D-2F?J5b@_*@L#^rkK0_p>c%(()~gP4*5^ z+vYi|w*VAKPg$(%7|wcE|K1`xoMAId{VEzQQ%OBKx8YSvmenr0 zTiKQ`{;L!5%6Bj5zZ-A7aN)a?f6KVL&2R-|b>1Hejk)4Af<$FG{obE8De<{5> zd(`hYrE(vcEf^oJlkR;bwp5Z^M7-QaRgh;PHDd#LB3vFVE+8~ku;atw&xyG*NOET zFFSOFVE%PZe9dJtGWe}$U;csSR)aUUn*4Eo+4!+Lj=%QhSz4cbw14=xOM>Ctu^X<+ znv0c6vw0A&{}24zh1ZjImHSddGlfP@lbJq7Iw#~b5F>xrVztv5-ugO(CQpN#=75_j z;5mSl4hPIVJqIJ5$#^PtbKkIBA?}aZPh0thm#Ssbr-3V)o|SPj{^BU5F(_*M3pjx3h^l3|9z z`VR3~=^1}nWJ;$*23xqelaUtNsxDL#pA_O`-f~DBDejt!z!-4IYIDoJMTW8TZ@*Fa z7AejBdb>qZ*DV>Os$eM-xuUiyh+qaf11x@BG%ZbGzL_4nrj@r98EK+c-p&+gXDnxhnZ)Xz}im9nQPQ*Vuo%IBB2G2@)5of5(-pH@>&FVl=Ds zU1On;==pyO$&43jYL~Ez_sB9kQMkHrx^TX5L*Yh^A5-Ye0N1Dft!z+i$NxDx=NqU-xB^HM~j9KY(w&XWR+m;tUA#MJ;$=lf-iT3=USEfBC*pB{^ zO%Jn^$>g!}FO`<6!l6G=T`C1d#)@lxhNaQW|Nj6&hlJk%004NLV_;-pU;tvvRek&8 z`E9;3a5KLEiZI;19JvaaX1>h48c1<4FoAz~09!!|xBvinoMT{QU|@d100b->KqT{J z21W){FarQS+_y;lw7znTlL}Ka50nqw5Ks_?5Uvo^5g-vl5n2(H650~< z6J`_Y6p9wq7l;@r7}gnp8gd%^8%i7m97Y_f9O@k)9XuUa9oimT9*!RhA8H^hAZ#GO zBVHuRC0HexCA1~dCkiNHEHo@)ES!HVwk*~yA}x3=_AZPr^e9GO$z_dK z72okaa~3@3Y@m7tySBH7cu#z2%wi|%y^utG@+jv4SOTa0Q@<#NW9D*E79YOzT4c9cuqnj&!^c|K)=63p0S1?x_$Dd|CKUreQ*vV7`|E~ZgQ zRt(IItQ<^8{Z~qI(w<5sk47mbO3rBgeZCrOrXs7yB!*ThN`V_zIx2rgHY1&l!qj6b ziX}zVn&vyJOPQ8qtxcfDm?6`4+)lQ8qKjHbJ&kW!Uz^vZiP^p6>V?Emh_j~#MmT3XpI}Cnb?(LeV^zmt&@Iam33xHJfy8rrps*XN*fcm>0}xD)b!p! z3P+5GI$`2rRAsuXqRoF@Y?cXpn0ThhrzD(m&yxCl6AI6nV&z)FwWknP<0|&D=Se(c znew}^EPX0PcUwLz6|FQ?uA=BGADeWGGC%7IKbLLT3Q2lw)k+r9fP+a42EUeU9_F%} zgrz;n|12dhW>S#JW|8uF`gyZC3)P$&o1~P@yA(^E2IBL%aUvO8D4#7xl`omo*>Y6b PF=;n8{sU@1pymJo_0tt? diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index 787e9dfbe5..4ce890ec9d 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -23,1027 +23,1027 @@ ], "iconDefinitions": { "_R_light": { - "fontCharacter": "\\E001", + "fontCharacter": "\\E075", "fontColor": "#498ba7" }, "_R": { - "fontCharacter": "\\E001", + "fontCharacter": "\\E075", "fontColor": "#519aba" }, "_argdown_light": { - "fontCharacter": "\\E003", + "fontCharacter": "\\E002", "fontColor": "#498ba7" }, "_argdown": { - "fontCharacter": "\\E003", + "fontCharacter": "\\E002", "fontColor": "#519aba" }, "_asm_light": { - "fontCharacter": "\\E004", + "fontCharacter": "\\E003", "fontColor": "#b8383d" }, "_asm": { - "fontCharacter": "\\E004", + "fontCharacter": "\\E003", "fontColor": "#cc3e44" }, "_audio_light": { - "fontCharacter": "\\E005", + "fontCharacter": "\\E004", "fontColor": "#9068b0" }, "_audio": { - "fontCharacter": "\\E005", + "fontCharacter": "\\E004", "fontColor": "#a074c4" }, "_babel_light": { - "fontCharacter": "\\E006", + "fontCharacter": "\\E005", "fontColor": "#b7b73b" }, "_babel": { - "fontCharacter": "\\E006", + "fontCharacter": "\\E005", "fontColor": "#cbcb41" }, "_bower_light": { - "fontCharacter": "\\E007", + "fontCharacter": "\\E006", "fontColor": "#cc6d2e" }, "_bower": { - "fontCharacter": "\\E007", + "fontCharacter": "\\E006", "fontColor": "#e37933" }, "_bsl_light": { - "fontCharacter": "\\E008", + "fontCharacter": "\\E007", "fontColor": "#b8383d" }, "_bsl": { - "fontCharacter": "\\E008", + "fontCharacter": "\\E007", "fontColor": "#cc3e44" }, "_c_light": { - "fontCharacter": "\\E00A", + "fontCharacter": "\\E009", "fontColor": "#498ba7" }, "_c": { - "fontCharacter": "\\E00A", + "fontCharacter": "\\E009", "fontColor": "#519aba" }, "_c-sharp_light": { - "fontCharacter": "\\E009", + "fontCharacter": "\\E008", "fontColor": "#498ba7" }, "_c-sharp": { - "fontCharacter": "\\E009", + "fontCharacter": "\\E008", "fontColor": "#519aba" }, "_c_1_light": { - "fontCharacter": "\\E00A", + "fontCharacter": "\\E009", "fontColor": "#9068b0" }, "_c_1": { - "fontCharacter": "\\E00A", + "fontCharacter": "\\E009", "fontColor": "#a074c4" }, "_c_2_light": { - "fontCharacter": "\\E00A", + "fontCharacter": "\\E009", "fontColor": "#b7b73b" }, "_c_2": { - "fontCharacter": "\\E00A", + "fontCharacter": "\\E009", "fontColor": "#cbcb41" }, "_cake_light": { - "fontCharacter": "\\E00B", + "fontCharacter": "\\E00A", "fontColor": "#b8383d" }, "_cake": { - "fontCharacter": "\\E00B", + "fontCharacter": "\\E00A", "fontColor": "#cc3e44" }, "_cake_php_light": { - "fontCharacter": "\\E00C", + "fontCharacter": "\\E00B", "fontColor": "#b8383d" }, "_cake_php": { - "fontCharacter": "\\E00C", + "fontCharacter": "\\E00B", "fontColor": "#cc3e44" }, "_clock_light": { - "fontCharacter": "\\E010", + "fontCharacter": "\\E00F", "fontColor": "#498ba7" }, "_clock": { - "fontCharacter": "\\E010", + "fontCharacter": "\\E00F", "fontColor": "#519aba" }, "_clock_1_light": { - "fontCharacter": "\\E010", + "fontCharacter": "\\E00F", "fontColor": "#627379" }, "_clock_1": { - "fontCharacter": "\\E010", + "fontCharacter": "\\E00F", "fontColor": "#6d8086" }, "_clojure_light": { - "fontCharacter": "\\E011", + "fontCharacter": "\\E010", "fontColor": "#7fae42" }, "_clojure": { - "fontCharacter": "\\E011", + "fontCharacter": "\\E010", "fontColor": "#8dc149" }, "_clojure_1_light": { - "fontCharacter": "\\E011", + "fontCharacter": "\\E010", "fontColor": "#498ba7" }, "_clojure_1": { - "fontCharacter": "\\E011", + "fontCharacter": "\\E010", "fontColor": "#519aba" }, "_code-climate_light": { - "fontCharacter": "\\E012", + "fontCharacter": "\\E011", "fontColor": "#7fae42" }, "_code-climate": { - "fontCharacter": "\\E012", + "fontCharacter": "\\E011", "fontColor": "#8dc149" }, "_code-search_light": { - "fontCharacter": "\\E013", + "fontCharacter": "\\E012", "fontColor": "#9068b0" }, "_code-search": { - "fontCharacter": "\\E013", + "fontCharacter": "\\E012", "fontColor": "#a074c4" }, "_coffee_light": { - "fontCharacter": "\\E014", + "fontCharacter": "\\E013", "fontColor": "#b7b73b" }, "_coffee": { - "fontCharacter": "\\E014", + "fontCharacter": "\\E013", "fontColor": "#cbcb41" }, "_coldfusion_light": { - "fontCharacter": "\\E016", + "fontCharacter": "\\E015", "fontColor": "#498ba7" }, "_coldfusion": { - "fontCharacter": "\\E016", + "fontCharacter": "\\E015", "fontColor": "#519aba" }, "_config_light": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E016", "fontColor": "#627379" }, "_config": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E016", "fontColor": "#6d8086" }, "_cpp_light": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E017", "fontColor": "#498ba7" }, "_cpp": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E017", "fontColor": "#519aba" }, "_cpp_1_light": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E017", "fontColor": "#9068b0" }, "_cpp_1": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E017", "fontColor": "#a074c4" }, "_cpp_2_light": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E017", "fontColor": "#b7b73b" }, "_cpp_2": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E017", "fontColor": "#cbcb41" }, "_crystal_light": { - "fontCharacter": "\\E019", + "fontCharacter": "\\E018", "fontColor": "#bfc2c1" }, "_crystal": { - "fontCharacter": "\\E019", + "fontCharacter": "\\E018", "fontColor": "#d4d7d6" }, "_crystal_embedded_light": { - "fontCharacter": "\\E01A", + "fontCharacter": "\\E019", "fontColor": "#bfc2c1" }, "_crystal_embedded": { - "fontCharacter": "\\E01A", + "fontCharacter": "\\E019", "fontColor": "#d4d7d6" }, "_css_light": { - "fontCharacter": "\\E01B", + "fontCharacter": "\\E01A", "fontColor": "#498ba7" }, "_css": { - "fontCharacter": "\\E01B", + "fontCharacter": "\\E01A", "fontColor": "#519aba" }, "_csv_light": { - "fontCharacter": "\\E01C", + "fontCharacter": "\\E01B", "fontColor": "#7fae42" }, "_csv": { - "fontCharacter": "\\E01C", + "fontCharacter": "\\E01B", "fontColor": "#8dc149" }, "_cu_light": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01C", "fontColor": "#7fae42" }, "_cu": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01C", "fontColor": "#8dc149" }, "_cu_1_light": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01C", "fontColor": "#9068b0" }, "_cu_1": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01C", "fontColor": "#a074c4" }, "_d_light": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01D", "fontColor": "#b8383d" }, "_d": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01D", "fontColor": "#cc3e44" }, "_dart_light": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E01E", "fontColor": "#498ba7" }, "_dart": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E01E", "fontColor": "#519aba" }, "_db_light": { - "fontCharacter": "\\E020", + "fontCharacter": "\\E01F", "fontColor": "#dd4b78" }, "_db": { - "fontCharacter": "\\E020", + "fontCharacter": "\\E01F", "fontColor": "#f55385" }, "_default_light": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E020", "fontColor": "#bfc2c1" }, "_default": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E020", "fontColor": "#d4d7d6" }, "_docker_light": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#498ba7" }, "_docker": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#519aba" }, "_docker_1_light": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#455155" }, "_docker_1": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#4d5a5e" }, "_docker_2_light": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#7fae42" }, "_docker_2": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#8dc149" }, "_docker_3_light": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#dd4b78" }, "_docker_3": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E022", "fontColor": "#f55385" }, "_ejs_light": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E024", "fontColor": "#b7b73b" }, "_ejs": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E024", "fontColor": "#cbcb41" }, "_elixir_light": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E025", "fontColor": "#9068b0" }, "_elixir": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E025", "fontColor": "#a074c4" }, "_elixir_script_light": { - "fontCharacter": "\\E027", + "fontCharacter": "\\E026", "fontColor": "#9068b0" }, "_elixir_script": { - "fontCharacter": "\\E027", + "fontCharacter": "\\E026", "fontColor": "#a074c4" }, "_elm_light": { - "fontCharacter": "\\E028", + "fontCharacter": "\\E027", "fontColor": "#498ba7" }, "_elm": { - "fontCharacter": "\\E028", + "fontCharacter": "\\E027", "fontColor": "#519aba" }, "_eslint_light": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E029", "fontColor": "#9068b0" }, "_eslint": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E029", "fontColor": "#a074c4" }, "_eslint_1_light": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E029", "fontColor": "#455155" }, "_eslint_1": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E029", "fontColor": "#4d5a5e" }, "_ethereum_light": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02A", "fontColor": "#498ba7" }, "_ethereum": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02A", "fontColor": "#519aba" }, "_f-sharp_light": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02B", "fontColor": "#498ba7" }, "_f-sharp": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02B", "fontColor": "#519aba" }, "_favicon_light": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02C", "fontColor": "#b7b73b" }, "_favicon": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02C", "fontColor": "#cbcb41" }, "_firebase_light": { - "fontCharacter": "\\E02E", + "fontCharacter": "\\E02D", "fontColor": "#cc6d2e" }, "_firebase": { - "fontCharacter": "\\E02E", + "fontCharacter": "\\E02D", "fontColor": "#e37933" }, "_firefox_light": { - "fontCharacter": "\\E02F", + "fontCharacter": "\\E02E", "fontColor": "#cc6d2e" }, "_firefox": { - "fontCharacter": "\\E02F", + "fontCharacter": "\\E02E", "fontColor": "#e37933" }, "_font_light": { - "fontCharacter": "\\E031", + "fontCharacter": "\\E030", "fontColor": "#b8383d" }, "_font": { - "fontCharacter": "\\E031", + "fontCharacter": "\\E030", "fontColor": "#cc3e44" }, "_git_light": { - "fontCharacter": "\\E032", + "fontCharacter": "\\E031", "fontColor": "#3b4b52" }, "_git": { - "fontCharacter": "\\E032", + "fontCharacter": "\\E031", "fontColor": "#41535b" }, "_github_light": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E032", "fontColor": "#bfc2c1" }, "_github": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E032", "fontColor": "#d4d7d6" }, "_go_light": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E035", "fontColor": "#498ba7" }, "_go": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E035", "fontColor": "#519aba" }, "_go2_light": { - "fontCharacter": "\\E037", + "fontCharacter": "\\E036", "fontColor": "#498ba7" }, "_go2": { - "fontCharacter": "\\E037", + "fontCharacter": "\\E036", "fontColor": "#519aba" }, "_gradle_light": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E037", "fontColor": "#498ba7" }, "_gradle": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E037", "fontColor": "#519aba" }, "_grails_light": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E038", "fontColor": "#7fae42" }, "_grails": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E038", "fontColor": "#8dc149" }, "_graphql_light": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E039", "fontColor": "#dd4b78" }, "_graphql": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E039", "fontColor": "#f55385" }, "_grunt_light": { - "fontCharacter": "\\E03B", + "fontCharacter": "\\E03A", "fontColor": "#cc6d2e" }, "_grunt": { - "fontCharacter": "\\E03B", + "fontCharacter": "\\E03A", "fontColor": "#e37933" }, "_gulp_light": { - "fontCharacter": "\\E03C", + "fontCharacter": "\\E03B", "fontColor": "#b8383d" }, "_gulp": { - "fontCharacter": "\\E03C", + "fontCharacter": "\\E03B", "fontColor": "#cc3e44" }, "_haml_light": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03D", "fontColor": "#b8383d" }, "_haml": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03D", "fontColor": "#cc3e44" }, "_happenings_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E03E", "fontColor": "#498ba7" }, "_happenings": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E03E", "fontColor": "#519aba" }, "_haskell_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E03F", "fontColor": "#9068b0" }, "_haskell": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E03F", "fontColor": "#a074c4" }, "_haxe_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#cc6d2e" }, "_haxe": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#e37933" }, "_haxe_1_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#b7b73b" }, "_haxe_1": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#cbcb41" }, "_haxe_2_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#498ba7" }, "_haxe_2": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#519aba" }, "_haxe_3_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#9068b0" }, "_haxe_3": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E040", "fontColor": "#a074c4" }, "_heroku_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E041", "fontColor": "#9068b0" }, "_heroku": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E041", "fontColor": "#a074c4" }, "_hex_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E042", "fontColor": "#b8383d" }, "_hex": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E042", "fontColor": "#cc3e44" }, "_html_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#498ba7" }, "_html": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#519aba" }, "_html_1_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#7fae42" }, "_html_1": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#8dc149" }, "_html_2_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#b7b73b" }, "_html_2": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#cbcb41" }, "_html_3_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#cc6d2e" }, "_html_3": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E043", "fontColor": "#e37933" }, "_html_erb_light": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E044", "fontColor": "#b8383d" }, "_html_erb": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E044", "fontColor": "#cc3e44" }, "_ignored_light": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E045", "fontColor": "#3b4b52" }, "_ignored": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E045", "fontColor": "#41535b" }, "_illustrator_light": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E046", "fontColor": "#b7b73b" }, "_illustrator": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E046", "fontColor": "#cbcb41" }, "_image_light": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E047", "fontColor": "#9068b0" }, "_image": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E047", "fontColor": "#a074c4" }, "_info_light": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E048", "fontColor": "#498ba7" }, "_info": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E048", "fontColor": "#519aba" }, "_ionic_light": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E049", "fontColor": "#498ba7" }, "_ionic": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E049", "fontColor": "#519aba" }, "_jade_light": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04A", "fontColor": "#b8383d" }, "_jade": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04A", "fontColor": "#cc3e44" }, "_java_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04B", "fontColor": "#b8383d" }, "_java": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04B", "fontColor": "#cc3e44" }, "_javascript_light": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04C", "fontColor": "#b7b73b" }, "_javascript": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04C", "fontColor": "#cbcb41" }, "_javascript_1_light": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04C", "fontColor": "#cc6d2e" }, "_javascript_1": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04C", "fontColor": "#e37933" }, "_javascript_2_light": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04C", "fontColor": "#498ba7" }, "_javascript_2": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04C", "fontColor": "#519aba" }, "_jenkins_light": { - "fontCharacter": "\\E04E", + "fontCharacter": "\\E04D", "fontColor": "#b8383d" }, "_jenkins": { - "fontCharacter": "\\E04E", + "fontCharacter": "\\E04D", "fontColor": "#cc3e44" }, "_jinja_light": { - "fontCharacter": "\\E04F", + "fontCharacter": "\\E04E", "fontColor": "#b8383d" }, "_jinja": { - "fontCharacter": "\\E04F", + "fontCharacter": "\\E04E", "fontColor": "#cc3e44" }, "_json_light": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E04F", "fontColor": "#b7b73b" }, "_json": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E04F", "fontColor": "#cbcb41" }, "_json_1_light": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E04F", "fontColor": "#7fae42" }, "_json_1": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E04F", "fontColor": "#8dc149" }, "_julia_light": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E051", "fontColor": "#9068b0" }, "_julia": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E051", "fontColor": "#a074c4" }, "_karma_light": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E052", "fontColor": "#7fae42" }, "_karma": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E052", "fontColor": "#8dc149" }, "_kotlin_light": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E053", "fontColor": "#cc6d2e" }, "_kotlin": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E053", "fontColor": "#e37933" }, "_less_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E054", "fontColor": "#498ba7" }, "_less": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E054", "fontColor": "#519aba" }, "_license_light": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E055", "fontColor": "#b7b73b" }, "_license": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E055", "fontColor": "#cbcb41" }, "_license_1_light": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E055", "fontColor": "#cc6d2e" }, "_license_1": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E055", "fontColor": "#e37933" }, "_license_2_light": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E055", "fontColor": "#b8383d" }, "_license_2": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E055", "fontColor": "#cc3e44" }, "_liquid_light": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E056", "fontColor": "#7fae42" }, "_liquid": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E056", "fontColor": "#8dc149" }, "_livescript_light": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E057", "fontColor": "#498ba7" }, "_livescript": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E057", "fontColor": "#519aba" }, "_lock_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E058", "fontColor": "#7fae42" }, "_lock": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E058", "fontColor": "#8dc149" }, "_lua_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E059", "fontColor": "#498ba7" }, "_lua": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E059", "fontColor": "#519aba" }, "_makefile_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#cc6d2e" }, "_makefile": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#e37933" }, "_makefile_1_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#9068b0" }, "_makefile_1": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#a074c4" }, "_makefile_2_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#627379" }, "_makefile_2": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#6d8086" }, "_makefile_3_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#498ba7" }, "_makefile_3": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05A", "fontColor": "#519aba" }, "_markdown_light": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05B", "fontColor": "#498ba7" }, "_markdown": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05B", "fontColor": "#519aba" }, "_maven_light": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05C", "fontColor": "#b8383d" }, "_maven": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05C", "fontColor": "#cc3e44" }, "_mdo_light": { - "fontCharacter": "\\E05E", + "fontCharacter": "\\E05D", "fontColor": "#b8383d" }, "_mdo": { - "fontCharacter": "\\E05E", + "fontCharacter": "\\E05D", "fontColor": "#cc3e44" }, "_mustache_light": { - "fontCharacter": "\\E05F", + "fontCharacter": "\\E05E", "fontColor": "#cc6d2e" }, "_mustache": { - "fontCharacter": "\\E05F", + "fontCharacter": "\\E05E", "fontColor": "#e37933" }, "_nim_light": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E060", "fontColor": "#b7b73b" }, "_nim": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E060", "fontColor": "#cbcb41" }, "_notebook_light": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E061", "fontColor": "#498ba7" }, "_notebook": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E061", "fontColor": "#519aba" }, "_npm_light": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E062", "fontColor": "#3b4b52" }, "_npm": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E062", "fontColor": "#41535b" }, "_npm_1_light": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E062", "fontColor": "#b8383d" }, "_npm_1": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E062", "fontColor": "#cc3e44" }, "_npm_ignored_light": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E063", "fontColor": "#3b4b52" }, "_npm_ignored": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E063", "fontColor": "#41535b" }, "_nunjucks_light": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E064", "fontColor": "#7fae42" }, "_nunjucks": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E064", "fontColor": "#8dc149" }, "_ocaml_light": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E065", "fontColor": "#cc6d2e" }, "_ocaml": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E065", "fontColor": "#e37933" }, "_odata_light": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E066", "fontColor": "#cc6d2e" }, "_odata": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E066", "fontColor": "#e37933" }, "_pddl_light": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E067", "fontColor": "#9068b0" }, "_pddl": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E067", "fontColor": "#a074c4" }, "_pdf_light": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E068", "fontColor": "#b8383d" }, "_pdf": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E068", "fontColor": "#cc3e44" }, "_perl_light": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E069", "fontColor": "#498ba7" }, "_perl": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E069", "fontColor": "#519aba" }, "_photoshop_light": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06A", "fontColor": "#498ba7" }, "_photoshop": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06A", "fontColor": "#519aba" }, "_php_light": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06B", "fontColor": "#9068b0" }, "_php": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06B", "fontColor": "#a074c4" }, "_plan_light": { - "fontCharacter": "\\E06D", + "fontCharacter": "\\E06C", "fontColor": "#7fae42" }, "_plan": { - "fontCharacter": "\\E06D", + "fontCharacter": "\\E06C", "fontColor": "#8dc149" }, "_platformio_light": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06D", "fontColor": "#cc6d2e" }, "_platformio": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06D", "fontColor": "#e37933" }, "_powershell_light": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E06E", "fontColor": "#498ba7" }, "_powershell": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E06E", "fontColor": "#519aba" }, "_prisma_light": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E06F", "fontColor": "#498ba7" }, "_prisma": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E06F", "fontColor": "#519aba" }, "_prolog_light": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E071", "fontColor": "#cc6d2e" }, "_prolog": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E071", "fontColor": "#e37933" }, "_pug_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E072", "fontColor": "#b8383d" }, "_pug": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E072", "fontColor": "#cc3e44" }, "_puppet_light": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E073", "fontColor": "#b7b73b" }, "_puppet": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E073", "fontColor": "#cbcb41" }, "_python_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E074", "fontColor": "#519aba" }, "_react_light": { @@ -1198,223 +1198,231 @@ "fontCharacter": "\\E088", "fontColor": "#e37933" }, - "_svg_light": { + "_svelte_light": { "fontCharacter": "\\E089", + "fontColor": "#b8383d" + }, + "_svelte": { + "fontCharacter": "\\E089", + "fontColor": "#cc3e44" + }, + "_svg_light": { + "fontCharacter": "\\E08A", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08A", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08A", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08A", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08B", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08B", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E08E" + "fontCharacter": "\\E08F" }, "_tsconfig_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E090", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E090", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E091", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E091", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E093", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E093", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E094", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E094", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E095", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E095", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E096", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E096", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E097", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E097", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E098", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E098", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E099", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E099", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09B", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09B", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09C", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09C", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09D", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E09C", + "fontCharacter": "\\E09D", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09E", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E09D", + "fontCharacter": "\\E09E", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E09E", + "fontCharacter": "\\E09F", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E09E", + "fontCharacter": "\\E09F", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A0", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A0", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A0", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E09F", + "fontCharacter": "\\E0A0", "fontColor": "#6d8086" }, // {{SQL CARBON EDIT}} @@ -1559,6 +1567,7 @@ "scala": "_scala", "sol": "_ethereum", "styl": "_stylus", + "svelte": "_svelte", "tf": "_terraform", "tf.json": "_terraform", "tfvars": "_terraform", @@ -1773,6 +1782,7 @@ "haml": "_haml", "stylus": "_stylus", "vala": "_vala", + "github-issues": "_github", "todo": "_todo", // {{SQL CARBON EDIT}} "notebook": "notebook_dark", @@ -1909,6 +1919,7 @@ "scala": "_scala_light", "sol": "_ethereum_light", "styl": "_stylus_light", + "svelte": "_svelte_light", "tf": "_terraform_light", "tf.json": "_terraform_light", "tfvars": "_terraform_light", @@ -2063,6 +2074,7 @@ "haml": "_haml_light", "stylus": "_stylus_light", "vala": "_vala_light", + "github-issues": "_github_light", // {{SQL CARBON EDIT}} "notebook": "notebook", "scmp": "scmp", @@ -2129,5 +2141,5 @@ "Schema Compare": "scmp" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/4bbf2132df28c71302e305077ce20a811bf7d64b" -} \ No newline at end of file + "version": "https://github.com/jesseweed/seti-ui/commit/9c1c29d6e9358f9ae99bd3a4bf0d2fa804dca686" +} diff --git a/extensions/theme-seti/package.json b/extensions/theme-seti/package.json index bcdbb3209e..fd0dec2c8b 100644 --- a/extensions/theme-seti/package.json +++ b/extensions/theme-seti/package.json @@ -1,23 +1,29 @@ { - "name": "vscode-theme-seti", - "private": true, - "version": "1.0.0", - "displayName": "%displayName%", - "description": "%description%", - "publisher": "vscode", - "license": "MIT", - "icon": "icons/seti-circular-128x128.png", - "scripts": { - "update": "node ./build/update-icon-theme.js" - }, - "engines": { "vscode": "*" }, - "contributes": { - "iconThemes": [ - { - "id": "vs-seti", - "label": "%themeLabel%", - "path": "./icons/vs-seti-icon-theme.json" - } - ] - } + "name": "vscode-theme-seti", + "private": true, + "version": "1.0.0", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "vscode", + "license": "MIT", + "icon": "icons/seti-circular-128x128.png", + "scripts": { + "update": "node ./build/update-icon-theme.js" + }, + "engines": { + "vscode": "*" + }, + "contributes": { + "iconThemes": [ + { + "id": "vs-seti", + "label": "%themeLabel%", + "path": "./icons/vs-seti-icon-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-seti/yarn.lock b/extensions/theme-seti/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-seti/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-solarized-dark/package.json b/extensions/theme-solarized-dark/package.json index eb7dc5ff94..fee98f5946 100644 --- a/extensions/theme-solarized-dark/package.json +++ b/extensions/theme-solarized-dark/package.json @@ -1,19 +1,25 @@ { - "name": "theme-solarized-dark", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "themes": [ - { - "id": "Solarized Dark", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/solarized-dark-color-theme.json" - } - ] - } + "name": "theme-solarized-dark", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Solarized Dark", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/solarized-dark-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-solarized-dark/yarn.lock b/extensions/theme-solarized-dark/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-solarized-dark/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-solarized-light/package.json b/extensions/theme-solarized-light/package.json index 421aa7a825..7efd642d58 100644 --- a/extensions/theme-solarized-light/package.json +++ b/extensions/theme-solarized-light/package.json @@ -1,19 +1,25 @@ { - "name": "theme-solarized-light", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "themes": [ - { - "id": "Solarized Light", - "label": "%themeLabel%", - "uiTheme": "vs", - "path": "./themes/solarized-light-color-theme.json" - } - ] - } + "name": "theme-solarized-light", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Solarized Light", + "label": "%themeLabel%", + "uiTheme": "vs", + "path": "./themes/solarized-light-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-solarized-light/yarn.lock b/extensions/theme-solarized-light/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-solarized-light/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/theme-tomorrow-night-blue/package.json b/extensions/theme-tomorrow-night-blue/package.json index e03f05d8a5..a9a6405a94 100644 --- a/extensions/theme-tomorrow-night-blue/package.json +++ b/extensions/theme-tomorrow-night-blue/package.json @@ -1,19 +1,25 @@ { - "name": "theme-tomorrow-night-blue", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "themes": [ - { - "id": "Tomorrow Night Blue", - "label": "%themeLabel%", - "uiTheme": "vs-dark", - "path": "./themes/tomorrow-night-blue-color-theme.json" - } - ] - } + "name": "theme-tomorrow-night-blue", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "*" + }, + "contributes": { + "themes": [ + { + "id": "Tomorrow Night Blue", + "label": "%themeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/tomorrow-night-blue-color-theme.json" + } + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } } diff --git a/extensions/theme-tomorrow-night-blue/yarn.lock b/extensions/theme-tomorrow-night-blue/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/theme-tomorrow-night-blue/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/typescript-basics/build/update-grammars.js b/extensions/typescript-basics/build/update-grammars.js new file mode 100644 index 0000000000..1e9c3a079a --- /dev/null +++ b/extensions/typescript-basics/build/update-grammars.js @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +'use strict'; + +var updateGrammar = require('vscode-grammar-updater'); + +function removeDom(grammar) { + grammar.repository['support-objects'].patterns = grammar.repository['support-objects'].patterns.filter(pattern => { + if (pattern.match && pattern.match.match(/\b(HTMLElement|ATTRIBUTE_NODE|stopImmediatePropagation)\b/g)) { + return false; + } + return true; + }); + return grammar; +} + +function removeNodeTypes(grammar) { + grammar.repository['support-objects'].patterns = grammar.repository['support-objects'].patterns.filter(pattern => { + if (pattern.name) { + if (pattern.name.startsWith('support.variable.object.node') || pattern.name.startsWith('support.class.node.')) { + return false; + } + } + if (pattern.captures) { + if (Object.values(pattern.captures).some(capture => + capture.name && (capture.name.startsWith('support.variable.object.process') + || capture.name.startsWith('support.class.console')) + )) { + return false; + } + } + return true; + }); + return grammar; +} + +function patchJsdoctype(grammar) { + grammar.repository['jsdoctype'].patterns = grammar.repository['jsdoctype'].patterns.filter(pattern => { + if (pattern.name && pattern.name.indexOf('illegal') >= -1) { + return false; + } + return true; + }); + return grammar; +} + +function patchGrammar(grammar) { + return removeNodeTypes(removeDom(patchJsdoctype(grammar))); +} + +function adaptToJavaScript(grammar, replacementScope) { + grammar.name = 'JavaScript (with React support)'; + grammar.fileTypes = ['.js', '.jsx', '.es6', '.mjs', '.cjs']; + grammar.scopeName = `source${replacementScope}`; + + var fixScopeNames = function (rule) { + if (typeof rule.name === 'string') { + rule.name = rule.name.replace(/\.tsx/g, replacementScope); + } + if (typeof rule.contentName === 'string') { + rule.contentName = rule.contentName.replace(/\.tsx/g, replacementScope); + } + for (var property in rule) { + var value = rule[property]; + if (typeof value === 'object') { + fixScopeNames(value); + } + } + }; + + var repository = grammar.repository; + for (var key in repository) { + fixScopeNames(repository[key]); + } +} + +var tsGrammarRepo = 'microsoft/TypeScript-TmLanguage'; +updateGrammar.update(tsGrammarRepo, 'TypeScript.tmLanguage', './syntaxes/TypeScript.tmLanguage.json', grammar => patchGrammar(grammar)); +updateGrammar.update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', './syntaxes/TypeScriptReact.tmLanguage.json', grammar => patchGrammar(grammar)); +updateGrammar.update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', '../javascript/syntaxes/JavaScript.tmLanguage.json', grammar => adaptToJavaScript(patchGrammar(grammar), '.js')); +updateGrammar.update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', '../javascript/syntaxes/JavaScriptReact.tmLanguage.json', grammar => adaptToJavaScript(patchGrammar(grammar), '.js.jsx')); diff --git a/extensions/typescript-basics/yarn.lock b/extensions/typescript-basics/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/extensions/typescript-basics/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/docker/test/colorize-fixtures/Dockerfile b/extensions/vscode-colorize-tests/test/colorize-fixtures/Dockerfile similarity index 100% rename from extensions/docker/test/colorize-fixtures/Dockerfile rename to extensions/vscode-colorize-tests/test/colorize-fixtures/Dockerfile diff --git a/extensions/yaml/test/colorize-fixtures/issue-1550.yaml b/extensions/vscode-colorize-tests/test/colorize-fixtures/issue-1550.yaml similarity index 100% rename from extensions/yaml/test/colorize-fixtures/issue-1550.yaml rename to extensions/vscode-colorize-tests/test/colorize-fixtures/issue-1550.yaml diff --git a/extensions/yaml/test/colorize-fixtures/issue-4008.yaml b/extensions/vscode-colorize-tests/test/colorize-fixtures/issue-4008.yaml similarity index 100% rename from extensions/yaml/test/colorize-fixtures/issue-4008.yaml rename to extensions/vscode-colorize-tests/test/colorize-fixtures/issue-4008.yaml diff --git a/extensions/yaml/test/colorize-fixtures/issue-6303.yaml b/extensions/vscode-colorize-tests/test/colorize-fixtures/issue-6303.yaml similarity index 100% rename from extensions/yaml/test/colorize-fixtures/issue-6303.yaml rename to extensions/vscode-colorize-tests/test/colorize-fixtures/issue-6303.yaml diff --git a/extensions/markdown-basics/test/colorize-fixtures/test-33886.md b/extensions/vscode-colorize-tests/test/colorize-fixtures/test-33886.md similarity index 100% rename from extensions/markdown-basics/test/colorize-fixtures/test-33886.md rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test-33886.md diff --git a/extensions/xml/test/colorize-fixtures/test-7115.xml b/extensions/vscode-colorize-tests/test/colorize-fixtures/test-7115.xml similarity index 100% rename from extensions/xml/test/colorize-fixtures/test-7115.xml rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test-7115.xml diff --git a/extensions/powershell/test/colorize-fixtures/test-freeze-56476.ps1 b/extensions/vscode-colorize-tests/test/colorize-fixtures/test-freeze-56476.ps1 similarity index 100% rename from extensions/powershell/test/colorize-fixtures/test-freeze-56476.ps1 rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test-freeze-56476.ps1 diff --git a/extensions/bat/test/colorize-fixtures/test.bat b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.bat similarity index 100% rename from extensions/bat/test/colorize-fixtures/test.bat rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.bat diff --git a/extensions/json/test/colorize-fixtures/test.json b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.json similarity index 100% rename from extensions/json/test/colorize-fixtures/test.json rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.json diff --git a/extensions/markdown-basics/test/colorize-fixtures/test.md b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.md similarity index 100% rename from extensions/markdown-basics/test/colorize-fixtures/test.md rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.md diff --git a/extensions/powershell/test/colorize-fixtures/test.ps1 b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.ps1 similarity index 100% rename from extensions/powershell/test/colorize-fixtures/test.ps1 rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.ps1 diff --git a/extensions/r/test/colorize-fixtures/test.r b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.r similarity index 100% rename from extensions/r/test/colorize-fixtures/test.r rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.r diff --git a/extensions/sql/test/colorize-fixtures/test.sql b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.sql similarity index 100% rename from extensions/sql/test/colorize-fixtures/test.sql rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.sql diff --git a/extensions/xml/test/colorize-fixtures/test.xml b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.xml similarity index 100% rename from extensions/xml/test/colorize-fixtures/test.xml rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.xml diff --git a/extensions/yaml/test/colorize-fixtures/test.yaml b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.yaml similarity index 100% rename from extensions/yaml/test/colorize-fixtures/test.yaml rename to extensions/vscode-colorize-tests/test/colorize-fixtures/test.yaml diff --git a/extensions/docker/test/colorize-results/Dockerfile.json b/extensions/vscode-colorize-tests/test/colorize-results/Dockerfile.json similarity index 100% rename from extensions/docker/test/colorize-results/Dockerfile.json rename to extensions/vscode-colorize-tests/test/colorize-results/Dockerfile.json diff --git a/extensions/yaml/test/colorize-results/issue-1550_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json similarity index 100% rename from extensions/yaml/test/colorize-results/issue-1550_yaml.json rename to extensions/vscode-colorize-tests/test/colorize-results/issue-1550_yaml.json diff --git a/extensions/yaml/test/colorize-results/issue-4008_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json similarity index 100% rename from extensions/yaml/test/colorize-results/issue-4008_yaml.json rename to extensions/vscode-colorize-tests/test/colorize-results/issue-4008_yaml.json diff --git a/extensions/yaml/test/colorize-results/issue-6303_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json similarity index 100% rename from extensions/yaml/test/colorize-results/issue-6303_yaml.json rename to extensions/vscode-colorize-tests/test/colorize-results/issue-6303_yaml.json diff --git a/extensions/markdown-basics/test/colorize-results/test-33886_md.json b/extensions/vscode-colorize-tests/test/colorize-results/test-33886_md.json similarity index 100% rename from extensions/markdown-basics/test/colorize-results/test-33886_md.json rename to extensions/vscode-colorize-tests/test/colorize-results/test-33886_md.json diff --git a/extensions/xml/test/colorize-results/test-7115_xml.json b/extensions/vscode-colorize-tests/test/colorize-results/test-7115_xml.json similarity index 100% rename from extensions/xml/test/colorize-results/test-7115_xml.json rename to extensions/vscode-colorize-tests/test/colorize-results/test-7115_xml.json diff --git a/extensions/powershell/test/colorize-results/test-freeze-56476_ps1.json b/extensions/vscode-colorize-tests/test/colorize-results/test-freeze-56476_ps1.json similarity index 100% rename from extensions/powershell/test/colorize-results/test-freeze-56476_ps1.json rename to extensions/vscode-colorize-tests/test/colorize-results/test-freeze-56476_ps1.json diff --git a/extensions/bat/test/colorize-results/test_bat.json b/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json similarity index 100% rename from extensions/bat/test/colorize-results/test_bat.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_bat.json diff --git a/extensions/json/test/colorize-results/test_json.json b/extensions/vscode-colorize-tests/test/colorize-results/test_json.json similarity index 100% rename from extensions/json/test/colorize-results/test_json.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_json.json diff --git a/extensions/markdown-basics/test/colorize-results/test_md.json b/extensions/vscode-colorize-tests/test/colorize-results/test_md.json similarity index 100% rename from extensions/markdown-basics/test/colorize-results/test_md.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_md.json diff --git a/extensions/powershell/test/colorize-results/test_ps1.json b/extensions/vscode-colorize-tests/test/colorize-results/test_ps1.json similarity index 100% rename from extensions/powershell/test/colorize-results/test_ps1.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_ps1.json diff --git a/extensions/r/test/colorize-results/test_r.json b/extensions/vscode-colorize-tests/test/colorize-results/test_r.json similarity index 100% rename from extensions/r/test/colorize-results/test_r.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_r.json diff --git a/extensions/sql/test/colorize-results/test_sql.json b/extensions/vscode-colorize-tests/test/colorize-results/test_sql.json similarity index 100% rename from extensions/sql/test/colorize-results/test_sql.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_sql.json diff --git a/extensions/xml/test/colorize-results/test_xml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_xml.json similarity index 100% rename from extensions/xml/test/colorize-results/test_xml.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_xml.json diff --git a/extensions/yaml/test/colorize-results/test_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json similarity index 100% rename from extensions/yaml/test/colorize-results/test_yaml.json rename to extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index 2c44fa07f8..603f2a6fc8 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -935,6 +935,108 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('workbench.action.files.save'); await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); + + test('cell execute command takes arguments', async () => { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + await vscode.commands.executeCommand('notebook.execute'); + assert.equal(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work + + const metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); + editor.document.metadata.runnable = true; + await metadataChangeEvent; + + await vscode.commands.executeCommand('notebook.execute'); + assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + + const clearChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.commands.executeCommand('notebook.cell.clearOutputs'); + await clearChangeEvent; + assert.equal(cell.outputs.length, 0, 'should clear'); + + const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.execute', { start: 0, end: 1 }, resource); + assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.equal(vscode.window.activeNotebookEditor?.document.uri.fsPath, secondResource.fsPath); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + test('document execute command takes arguments', async () => { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + await vscode.commands.executeCommand('notebook.execute'); + assert.equal(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work + + const metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); + editor.document.metadata.runnable = true; + await metadataChangeEvent; + + await vscode.commands.executeCommand('notebook.execute'); + assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + + const clearChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.commands.executeCommand('notebook.cell.clearOutputs'); + await clearChangeEvent; + assert.equal(cell.outputs.length, 0, 'should clear'); + + const secondResource = await createRandomFile('', undefined, 'second', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.execute', resource); + assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.equal(vscode.window.activeNotebookEditor?.document.uri.fsPath, secondResource.fsPath); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + test('cell execute and select kernel', async () => { + assertInitalState(); + const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(vscode.window.activeNotebookEditor !== undefined, true, 'notebook first'); + const editor = vscode.window.activeNotebookEditor!; + const cell = editor.document.cells[0]; + + const metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); + editor.document.metadata.runnable = true; + await metadataChangeEvent; + + await vscode.commands.executeCommand('notebook.cell.execute'); + assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.deepEqual((cell.outputs[0] as vscode.CellDisplayOutput).data, { + 'text/plain': [ + 'my output' + ] + }); + + await vscode.commands.executeCommand('notebook.selectKernel', { extension: 'vscode.vscode-notebook-tests', id: 'secondaryKernel' }) + await vscode.commands.executeCommand('notebook.cell.execute'); + assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked + assert.deepEqual((cell.outputs[0] as vscode.CellDisplayOutput).data, { + 'text/plain': [ + 'my second output' + ] + }); + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); }); suite('notebook dirty state', () => { @@ -1201,7 +1303,7 @@ suite('notebook working copy', () => { assert.notEqual(firstNotebookEditor, secondNotebookEditor); assert.equal(firstNotebookEditor?.document, secondNotebookEditor?.document, 'split notebook editors share the same document'); - assert.notEqual(firstNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png')), secondNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png'))); + // assert.notEqual(firstNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png')), secondNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png'))); await saveAllFilesAndCloseAll(resource); @@ -1269,11 +1371,10 @@ suite('regression', () => { await vscode.commands.executeCommand('vscode.open', cell.uri, vscode.ViewColumn.Active); assert.strictEqual(!!vscode.window.activeNotebookEditor, true); - assert.strictEqual(vscode.window.activeNotebookEditor?.document.uri.toString(), resource.toString()); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.uri.toString(), resource.toString()); }); - test('Cannot open notebook from cell-uri with vscode.open-command', async function () { - this.skip(); + test.skip('Cannot open notebook from cell-uri with vscode.open-command', async function () { assertInitalState(); const resource = await createRandomFile('', undefined, 'first', '.vsctestnb'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); @@ -1288,7 +1389,7 @@ suite('regression', () => { // removes the fragment if it matches something numeric. For notebooks that's not wanted... await vscode.commands.executeCommand('vscode.open', cell.uri); - assert.strictEqual(vscode.window.activeNotebookEditor?.document.uri.toString(), resource.toString()); + assert.strictEqual(vscode.window.activeNotebookEditor!.document.uri.toString(), resource.toString()); }); test('#97830, #97764. Support switch to other editor types', async function () { diff --git a/extensions/vscode-test-resolver/src/download.ts b/extensions/vscode-test-resolver/src/download.ts index 00732b01a4..be75c27dc5 100644 --- a/extensions/vscode-test-resolver/src/download.ts +++ b/extensions/vscode-test-resolver/src/download.ts @@ -23,14 +23,14 @@ function getDownloadUrl(updateUrl: string, commit: string, platform: string, qua return `${updateUrl}/commit:${commit}/server-${platform}/${quality}`; } -async function downloadVSCodeServerArchive(updateUrl: string, commit: string, quality: string, destDir: string): Promise { +async function downloadVSCodeServerArchive(updateUrl: string, commit: string, quality: string, destDir: string, log: (messsage: string) => void): Promise { ensureFolderExists(destDir); const platform = process.platform === 'win32' ? 'win32-x64' : process.platform === 'darwin' ? 'darwin' : 'linux-x64'; const downloadUrl = getDownloadUrl(updateUrl, commit, platform, quality); return new Promise((resolve, reject) => { - console.log(`Downloading VS Code Server from: ${downloadUrl}`); + log(`Downloading VS Code Server from: ${downloadUrl}`); const requestOptions: https.RequestOptions = parseUrl(downloadUrl); https.get(requestOptions, res => { @@ -70,9 +70,10 @@ async function downloadVSCodeServerArchive(updateUrl: string, commit: string, qu /** * Unzip a .zip or .tar.gz VS Code archive */ -function unzipVSCodeServer(vscodeArchivePath: string, extractDir: string) { +function unzipVSCodeServer(vscodeArchivePath: string, extractDir: string, destDir: string, log: (messsage: string) => void) { + log(`Extracting ${vscodeArchivePath}`); if (vscodeArchivePath.endsWith('.zip')) { - const tempDir = fs.mkdtempSync('vscode-server'); + const tempDir = fs.mkdtempSync(path.join(destDir, 'vscode-server-extract')); if (process.platform === 'win32') { cp.spawnSync('powershell.exe', [ '-NoProfile', @@ -95,17 +96,17 @@ function unzipVSCodeServer(vscodeArchivePath: string, extractDir: string) { } } -export async function downloadAndUnzipVSCodeServer(updateUrl: string, commit: string, quality: string = 'stable', destDir: string): Promise { +export async function downloadAndUnzipVSCodeServer(updateUrl: string, commit: string, quality: string = 'stable', destDir: string, log: (messsage: string) => void): Promise { const extractDir = path.join(destDir, commit); if (fs.existsSync(extractDir)) { - console.log(`Found ${extractDir}. Skipping download.`); + log(`Found ${extractDir}. Skipping download.`); } else { - console.log(`Downloading VS Code Server ${quality} - ${commit} into ${extractDir}.`); + log(`Downloading VS Code Server ${quality} - ${commit} into ${extractDir}.`); try { - const vscodeArchivePath = await downloadVSCodeServerArchive(updateUrl, commit, quality, destDir); + const vscodeArchivePath = await downloadVSCodeServerArchive(updateUrl, commit, quality, destDir, log); if (fs.existsSync(vscodeArchivePath)) { - unzipVSCodeServer(vscodeArchivePath, extractDir); + unzipVSCodeServer(vscodeArchivePath, extractDir, destDir, log); // Remove archive fs.unlinkSync(vscodeArchivePath); } @@ -114,4 +115,4 @@ export async function downloadAndUnzipVSCodeServer(updateUrl: string, commit: st } } return Promise.resolve(extractDir); -} \ No newline at end of file +} diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 81263ed15d..a25de1f879 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import * as net from 'net'; +import * as http from 'http'; import { downloadAndUnzipVSCodeServer } from './download'; import { terminateProcess } from './util/processes'; @@ -81,6 +82,7 @@ export function activate(context: vscode.ExtensionContext) { const commandArgs = ['--port=0', '--disable-telemetry']; const env = getNewEnv(); const remoteDataDir = process.env['TESTRESOLVER_DATA_FOLDER'] || path.join(os.homedir(), serverDataFolderName || `${dataFolderName}-testresolver`); + env['VSCODE_AGENT_FOLDER'] = remoteDataDir; outputChannel.appendLine(`Using data folder at ${remoteDataDir}`); @@ -90,15 +92,21 @@ export function activate(context: vscode.ExtensionContext) { const serverCommandPath = path.join(vscodePath, 'resources', 'server', 'bin-dev', serverCommand); extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath }); } else { + const extensionToInstall = process.env['TESTRESOLVER_INSTALL_BUILTIN_EXTENSION']; + if (extensionToInstall) { + commandArgs.push('--install-builtin-extension', extensionToInstall); + commandArgs.push('--start-server'); + } const serverCommand = process.platform === 'win32' ? 'server.cmd' : 'server.sh'; let serverLocation = env['VSCODE_REMOTE_SERVER_PATH']; // support environment variable to specify location of server on disk if (!serverLocation) { const serverBin = path.join(remoteDataDir, 'bin'); progress.report({ message: 'Installing VSCode Server' }); - serverLocation = await downloadAndUnzipVSCodeServer(updateUrl, commit, quality, serverBin); + serverLocation = await downloadAndUnzipVSCodeServer(updateUrl, commit, quality, serverBin, m => outputChannel.appendLine(m)); } outputChannel.appendLine(`Using server build at ${serverLocation}`); + outputChannel.appendLine(`Server arguments ${commandArgs.join(' ')}`); extHostProcess = cp.spawn(path.join(serverLocation, serverCommand), commandArgs, { env, cwd: serverLocation }); } @@ -206,27 +214,76 @@ export function activate(context: vscode.ExtensionContext) { }); } - vscode.workspace.registerRemoteAuthorityResolver('test', { + const authorityResolverDisposable = vscode.workspace.registerRemoteAuthorityResolver('test', { resolve(_authority: string): Thenable { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Open TestResolver Remote ([details](command:vscode-testresolver.showLog))', cancellable: false }, (progress) => doResolve(_authority, progress)); - } + }, + tunnelFactory, + tunnelFeatures: { elevation: true, public: !!vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') }, + showCandidatePort }); + context.subscriptions.push(authorityResolverDisposable); - vscode.commands.registerCommand('vscode-testresolver.newWindow', () => { + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.newWindow', () => { return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+test' }); - }); - vscode.commands.registerCommand('vscode-testresolver.newWindowWithError', () => { + })); + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.newWindowWithError', () => { return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+error' }); - }); - vscode.commands.registerCommand('vscode-testresolver.showLog', () => { + })); + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.killServerAndTriggerHandledError', () => { + authorityResolverDisposable.dispose(); + if (extHostProcess) { + terminateProcess(extHostProcess, context.extensionPath); + } + vscode.workspace.registerRemoteAuthorityResolver('test', { + async resolve(_authority: string): Promise { + setTimeout(async () => { + await vscode.window.showErrorMessage('Just a custom message.', { modal: true, useCustom: true }, 'OK', 'Great'); + }, 2000); + throw vscode.RemoteAuthorityResolverError.NotAvailable('Intentional Error', true); + } + }); + })); + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.showLog', () => { if (outputChannel) { outputChannel.show(); } - }); + })); + + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.openTunnel', async () => { + const result = await vscode.window.showInputBox({ + prompt: 'Enter the remote port for the tunnel', + value: '5000', + validateInput: input => /^[\d]+$/.test(input) ? undefined : 'Not a valid number' + }); + if (result) { + const port = Number.parseInt(result); + vscode.workspace.openTunnel({ + remoteAddress: { + host: 'localhost', + port: port + }, + localAddressPort: port + 1 + }); + } + + })); + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.startRemoteServer', async () => { + const result = await vscode.window.showInputBox({ + prompt: 'Enter the port for the remote server', + value: '5000', + validateInput: input => /^[\d]+$/.test(input) ? undefined : 'Not a valid number' + }); + if (result) { + runHTTPTestServer(Number.parseInt(result)); + } + + })); + vscode.commands.executeCommand('setContext', 'forwardedPortsViewEnabled', true); } type ActionItem = (vscode.MessageItem & { execute: () => void; }); @@ -287,3 +344,92 @@ function sleep(ms: number): Promise { function getConfiguration(id: string): T | undefined { return vscode.workspace.getConfiguration('testresolver').get(id); } + +const remoteServers: number[] = []; + +async function showCandidatePort(_host: string, port: number, _detail: string): Promise { + return remoteServers.includes(port) || port === 100; +} + +async function tunnelFactory(tunnelOptions: vscode.TunnelOptions, tunnelCreationOptions: vscode.TunnelCreationOptions): Promise { + outputChannel.appendLine(`Tunnel factory request: Remote ${tunnelOptions.remoteAddress.port} -> local ${tunnelOptions.localAddressPort}`); + if (tunnelCreationOptions.elevationRequired) { + await vscode.window.showInformationMessage('This is a fake elevation message. A real resolver would show a native elevation prompt.', { modal: true }, 'Ok'); + } + + return createTunnelService(); + + function newTunnel(localAddress: { host: string, port: number }) { + const onDidDispose: vscode.EventEmitter = new vscode.EventEmitter(); + let isDisposed = false; + return { + localAddress, + remoteAddress: tunnelOptions.remoteAddress, + public: !!vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') && tunnelOptions.public, + onDidDispose: onDidDispose.event, + dispose: () => { + if (!isDisposed) { + isDisposed = true; + onDidDispose.fire(); + } + } + }; + } + + function createTunnelService(): Promise { + return new Promise((res, _rej) => { + const proxyServer = net.createServer(proxySocket => { + const remoteSocket = net.createConnection({ host: tunnelOptions.remoteAddress.host, port: tunnelOptions.remoteAddress.port }); + remoteSocket.pipe(proxySocket); + proxySocket.pipe(remoteSocket); + }); + let localPort = 0; + + if (tunnelOptions.localAddressPort) { + // When the tunnelOptions include a localAddressPort, we should use that. + // However, the test resolver all runs on one machine, so if the localAddressPort is the same as the remote port, + // then we must use a different port number. + localPort = tunnelOptions.localAddressPort; + } else { + localPort = tunnelOptions.remoteAddress.port; + } + + if (localPort === tunnelOptions.remoteAddress.port) { + localPort += 1; + } + + // The test resolver can't actually handle privileged ports, it only pretends to. + if (localPort < 1024 && process.platform !== 'win32') { + localPort = 0; + } + proxyServer.listen(localPort, () => { + const localPort = (proxyServer.address()).port; + outputChannel.appendLine(`New test resolver tunnel service: Remote ${tunnelOptions.remoteAddress.port} -> local ${localPort}`); + const tunnel = newTunnel({ host: 'localhost', port: localPort }); + tunnel.onDidDispose(() => proxyServer.close()); + res(tunnel); + }); + }); + } +} + +function runHTTPTestServer(port: number): vscode.Disposable { + const server = http.createServer((_req, res) => { + res.writeHead(200); + res.end(`Hello, World from test server running on port ${port}!`); + }); + remoteServers.push(port); + server.listen(port); + const message = `Opened HTTP server on http://localhost:${port}`; + console.log(message); + outputChannel.appendLine(message); + return { + dispose: () => { + server.close(); + const index = remoteServers.indexOf(port); + if (index !== -1) { + remoteServers.splice(index, 1); + } + } + }; +} diff --git a/extensions/vscode-test-resolver/src/util/processes.ts b/extensions/vscode-test-resolver/src/util/processes.ts index 517c33d087..3dcc2629da 100644 --- a/extensions/vscode-test-resolver/src/util/processes.ts +++ b/extensions/vscode-test-resolver/src/util/processes.ts @@ -23,7 +23,7 @@ export function terminateProcess(p: cp.ChildProcess, extensionPath: string): Ter } else if (process.platform === 'darwin' || process.platform === 'linux') { try { const cmd = path.join(extensionPath, 'scripts', 'terminateProcess.sh'); - const result = cp.spawnSync(cmd, [process.pid.toString()]); + const result = cp.spawnSync(cmd, [p.pid.toString()]); if (result.error) { return { success: false, error: result.error }; } @@ -34,4 +34,4 @@ export function terminateProcess(p: cp.ChildProcess, extensionPath: string): Ter p.kill('SIGKILL'); } return { success: true }; -} \ No newline at end of file +} diff --git a/extensions/xml/package.json b/extensions/xml/package.json index 3e5042df70..4555ba757b 100644 --- a/extensions/xml/package.json +++ b/extensions/xml/package.json @@ -1,99 +1,117 @@ { - "name": "xml", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "engines": { "vscode": "*" }, - "contributes": { - "languages": [{ - "id": "xml", - "extensions": [ - ".xml", - ".xsd", - ".ascx", - ".atom", - ".axml", - ".bpmn", - ".cpt", - ".csl", - ".csproj", - ".csproj.user", - ".dita", - ".ditamap", - ".dtd", - ".ent", - ".mod", - ".dtml", - ".fsproj", - ".fxml", - ".iml", - ".isml", - ".jmx", - ".launch", - ".menu", - ".mxml", - ".nuspec", - ".opml", - ".owl", - ".proj", - ".props", - ".pt", - ".publishsettings", - ".pubxml", - ".pubxml.user", - ".rbxlx", - ".rbxmx", - ".rdf", - ".rng", - ".rss", - ".shproj", - ".storyboard", - ".svg", - ".targets", - ".tld", - ".tmx", - ".vbproj", - ".vbproj.user", - ".vcxproj", - ".vcxproj.filters", - ".wsdl", - ".wxi", - ".wxl", - ".wxs", - ".xaml", - ".xbl", - ".xib", - ".xlf", - ".xliff", - ".xpdl", - ".xul", - ".xoml" - ], - "firstLine" : "(\\<\\?xml.*)|(\\ { - console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - process.exit(1); -}); - -// Load all the gulpfiles only if running tasks other than the editor tasks -const build = path.join(__dirname, 'build'); -require('glob').sync('gulpfile.*.js', { cwd: build }) - .forEach(f => require(`./build/${f}`)); +require('./build/gulpfile'); diff --git a/package.json b/package.json index ff1b64bd8a..653498571d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "azuredatastudio", "version": "1.29.0", - "distro": "2c6adba5d5c27f8119165dfa988b318cfe91a208", + "distro": "8cfc58e86b2bf061cc59854ee7e8620f5d6a8864", "author": { "name": "Microsoft Corporation" }, @@ -13,43 +13,53 @@ "test-browser": "node test/unit/browser/index.js", "preinstall": "node build/npm/preinstall.js", "postinstall": "node build/npm/postinstall.js", - "compile": "gulp compile --max_old_space_size=4095", - "watch": "concurrently \"npm:watch-client\" \"npm:watch-extensions\"", + "compile": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile", + "watch": "npm-run-all -lp watch-client watch-extensions", "watchd": "deemon yarn watch", "watch-webd": "deemon yarn watch-web", "kill-watchd": "deemon --kill yarn watch", "kill-watch-webd": "deemon --kill yarn watch-web", "restart-watchd": "deemon --restart yarn watch", "restart-watch-webd": "deemon --restart yarn watch-web", - "watch-client": "gulp watch-client --max_old_space_size=4095", + "watch-client": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-client", "watch-clientd": "deemon yarn watch-client", "kill-watch-clientd": "deemon --kill yarn watch-client", - "watch-extensions": "gulp watch-extensions --max_old_space_size=4095", + "watch-extensions": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-extensions", "watch-extensionsd": "deemon yarn watch-extensions", "kill-watch-extensionsd": "deemon --kill yarn watch-extensions", "mocha": "mocha test/unit/node/all.js --delay", "precommit": "node build/hygiene.js", - "gulp": "gulp --max_old_space_size=8192", + "gulp": "node --max_old_space_size=8192 ./node_modules/gulp/bin/gulp.js", "electron": "node build/lib/electron", "7z": "7z", "update-grammars": "node build/npm/update-all-grammars.js", "update-localization-extension": "node build/npm/update-localization-extension.js", - "smoketest": "cd test/smoke && node test/index.js", + "smoketest": "cd test/smoke && yarn compile && node test/index.js", + "smoketest-no-compile": "cd test/smoke && node test/index.js", "download-builtin-extensions": "node build/lib/builtInExtensions.js", "monaco-compile-check": "tsc -p src/tsconfig.monaco.json --noEmit", + "tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.tsec.json", "strict-vscode": "node --max_old_space_size=4095 node_modules/typescript/bin/tsc -p src/tsconfig.vscode.json", "strict-vscode-watch": "node --max_old_space_size=4095 node_modules/typescript/bin/tsc -p src/tsconfig.vscode.json --watch", "strict-initialization-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictPropertyInitialization", - "tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.json --noEmit", "valid-layers-check": "node --max_old_space_size=4095 build/lib/layersChecker.js", "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", "web": "node resources/web/code-web.js", - "compile-web": "gulp compile-web --max_old_space_size=4095", - "watch-web": "gulp watch-web --max_old_space_size=4095", - "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions", + "compile-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-web", + "watch-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-web", + "eslint": "node build/eslint", "sqllint": "eslint --no-eslintrc -c .eslintrc.sql.ts.json --rulesdir ./build/lib/eslint --ext .ts ./src/sql", - "electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.0.2" + "electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.2.2", + "playwright-install": "node build/azure-pipelines/common/installPlaywright.js", + "compile-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-build", + "compile-extensions-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-extensions-build", + "minify-vscode": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode", + "minify-vscode-reh": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh", + "minify-vscode-reh-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web", + "hygiene": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js hygiene", + "core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci", + "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci" }, "dependencies": { "@angular/animations": "~4.1.3", @@ -64,7 +74,7 @@ "ansi_up": "^3.0.0", "applicationinsights": "1.0.8", "chart.js": "^2.9.4", - "chokidar": "3.4.3", + "chokidar": "3.5.1", "graceful-fs": "4.2.3", "html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6", "http-proxy-agent": "^2.1.0", @@ -79,7 +89,7 @@ "native-keymap": "2.2.1", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", - "node-pty": "0.10.0-beta17", + "node-pty": "0.10.0-beta19", "plotly.js-dist-min": "^1.53.0", "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", @@ -98,10 +108,10 @@ "vscode-ripgrep": "^1.11.1", "vscode-sqlite3": "4.0.10", "vscode-textmate": "5.2.0", - "xterm": "4.10.0-beta.4", - "xterm-addon-search": "0.8.0-beta.3", + "xterm": "4.11.0-beta.2", + "xterm-addon-search": "0.8.0", "xterm-addon-unicode11": "0.3.0-beta.3", - "xterm-addon-webgl": "0.10.0-beta.1", + "xterm-addon-webgl": "0.10.0-beta.2", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.8.4" @@ -112,19 +122,22 @@ "@types/chart.js": "2.9.4", "@types/chokidar": "2.1.3", "@types/cookie": "^0.3.3", - "@types/debug": "^4.1.5", + "@types/copy-webpack-plugin": "^6.0.3", + "@types/cssnano": "^4.0.0", + "@types/debug": "4.1.5", "@types/graceful-fs": "4.1.2", + "@types/gulp-postcss": "^8.0.0", "@types/http-proxy-agent": "^2.0.1", "@types/keytar": "^4.4.0", - "@types/minimist": "^1.2.0", - "@types/mocha": "2.2.39", - "@types/node": "^12.11.7", + "@types/minimist": "^1.2.1", + "@types/mocha": "^8.2.0", + "@types/node": "^12.19.9", "@types/plotly.js": "^1.44.9", "@types/sanitize-html": "^1.18.2", "@types/sinon": "^1.16.36", "@types/trusted-types": "^1.0.6", "@types/vscode-windows-registry": "^1.0.0", - "@types/webpack": "^4.4.10", + "@types/webpack": "^4.41.25", "@types/windows-foreground-love": "^0.3.0", "@types/windows-mutex": "^0.4.0", "@types/windows-process-tree": "^0.2.0", @@ -137,12 +150,13 @@ "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "concurrently": "^5.2.0", - "copy-webpack-plugin": "^4.5.2", + "copy-webpack-plugin": "^6.0.3", "cson-parser": "^1.3.3", "css-loader": "^3.2.0", + "cssnano": "^4.1.10", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "9.4.3", + "electron": "11.2.2", "electron-rebuild": "2.0.3", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", @@ -153,21 +167,24 @@ "file-loader": "^4.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", - "gulp-atom-electron": "^1.22.0", + "gulp-atom-electron": "1.22.0", "gulp-azure-storage": "^0.11.1", + "gulp-bom": "^3.0.0", "gulp-buffer": "0.0.2", "gulp-concat": "^2.6.1", - "gulp-cssnano": "^2.1.3", "gulp-eslint": "^5.0.0", "gulp-filter": "^5.1.0", "gulp-flatmap": "^1.0.2", "gulp-gunzip": "^1.0.0", + "gulp-gzip": "^1.4.2", "gulp-json-editor": "^2.5.0", "gulp-plumber": "^1.2.0", + "gulp-postcss": "^9.0.0", "gulp-remote-retry-src": "^0.6.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", "gulp-shell": "^0.6.5", + "gulp-sourcemaps": "^3.0.0", "gulp-tsb": "4.0.5", "gulp-untar": "^0.0.7", "gulp-vinyl-zip": "^2.1.2", @@ -184,37 +201,40 @@ "merge-options": "^1.0.1", "mime": "^1.4.1", "minimatch": "^3.0.4", - "mkdirp": "^0.5.0", - "mocha": "^2.2.5", - "mocha-junit-reporter": "^1.23.1", + "minimist": "^1.2.5", + "mkdirp": "^1.0.4", + "mocha": "^8.2.1", + "mocha-junit-reporter": "^2.0.0", "mocha-multi-reporters": "^1.5.1", + "npm-run-all": "^4.1.5", "opn": "^6.0.0", "optimist": "0.3.5", "p-all": "^1.0.0", - "playwright": "1.6.2", + "playwright": "1.8.0", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", + "request": "^2.85.0", "rimraf": "^2.2.8", "sinon": "^1.17.2", - "source-map": "^0.4.4", + "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^1.0.0", "temp-write": "^3.4.0", - "ts-loader": "^4.4.2", - "tsec": "googleinterns/tsec", + "ts-loader": "^6.2.1", "typemoq": "^0.3.2", - "typescript": "^4.2.0-dev.20201119", + "typescript": "4.2.0-dev.20201207", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", - "vsce": "1.48.0", - "vscode-debugprotocol": "1.41.0", + "vscode-debugprotocol": "1.44.0", "vscode-nls-dev": "^3.3.1", + "vscode-telemetry-extractor": "^1.6.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", "webpack-stream": "^5.2.1", + "xml2js": "^0.4.17", "yaserver": "^0.2.0" }, "repository": { @@ -225,10 +245,14 @@ "url": "https://github.com/Microsoft/azuredatastudio/issues" }, "optionalDependencies": { - "vscode-windows-ca-certs": "0.2.0", + "vscode-windows-ca-certs": "^0.3.0", "vscode-windows-registry": "1.0.3", "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" + }, + "resolutions": { + "elliptic": "^6.5.3", + "nwmatcher": "^1.4.4" } } diff --git a/product.json b/product.json index 964819e65e..c4a328edf0 100644 --- a/product.json +++ b/product.json @@ -36,7 +36,7 @@ "gettingStartedUrl": "https://go.microsoft.com/fwlink/?linkid=862039", "releaseNotesUrl": "https://go.microsoft.com/fwlink/?linkid=875578", "documentationUrl": "https://go.microsoft.com/fwlink/?linkid=862277", - "vscodeVersion": "1.51.0", + "vscodeVersion": "1.53.0", "commit": "9ca6200018fc206d67a47229f991901a8a453781", "date": "2017-12-15T12:00:00.000Z", "recommendedExtensions": [ @@ -76,6 +76,7 @@ "ms-vscode.github-richnav" ], "extensionsGallery": { + "version": "0.0.76", "serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json" }, "builtInExtensions": [ diff --git a/remote/.yarnrc b/remote/.yarnrc index c1a32ce532..cd436416b5 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,3 +1,3 @@ disturl "http://nodejs.org/dist" -target "12.14.1" +target "12.18.3" runtime "node" diff --git a/remote/package.json b/remote/package.json index 75b65d88f7..fb8972605e 100644 --- a/remote/package.json +++ b/remote/package.json @@ -1,6 +1,7 @@ { "name": "vscode-reh", "version": "0.0.0", + "private": true, "dependencies": { "@angular/animations": "~4.1.3", "@angular/common": "~4.1.3", @@ -14,7 +15,7 @@ "angular2-grid": "2.0.6", "ansi_up": "^3.0.0", "chart.js": "^2.9.4", - "chokidar": "3.4.3", + "chokidar": "3.5.1", "cookie": "^0.4.0", "graceful-fs": "4.2.3", "html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6", @@ -27,7 +28,7 @@ "minimist": "^1.2.5", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", - "node-pty": "0.10.0-beta17", + "node-pty": "0.10.0-beta19", "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", "sanitize-html": "^1.19.1", @@ -40,19 +41,19 @@ "vscode-nsfw": "1.2.9", "vscode-oniguruma": "1.3.1", "vscode-proxy-agent": "^0.5.2", + "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.1", "vscode-textmate": "5.2.0", - "vscode-regexpp": "^3.1.0", - "xterm": "4.10.0-beta.4", - "xterm-addon-search": "0.8.0-beta.3", + "xterm": "4.11.0-beta.2", + "xterm-addon-search": "0.8.0", "xterm-addon-unicode11": "0.3.0-beta.3", - "xterm-addon-webgl": "0.10.0-beta.1", + "xterm-addon-webgl": "0.10.0-beta.2", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.8.4" }, "optionalDependencies": { - "vscode-windows-ca-certs": "0.2.0", + "vscode-windows-ca-certs": "0.3.0", "vscode-windows-registry": "1.0.2" } } diff --git a/remote/web/package.json b/remote/web/package.json index aaee19718c..ef5fd84745 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -1,6 +1,7 @@ { "name": "vscode-web", "version": "0.0.0", + "private": true, "dependencies": { "@angular/animations": "~4.1.3", "@angular/common": "~4.1.3", @@ -28,9 +29,9 @@ "turndown-plugin-gfm": "^1.0.2", "vscode-oniguruma": "1.3.1", "vscode-textmate": "5.2.0", - "xterm": "4.10.0-beta.4", - "xterm-addon-search": "0.8.0-beta.3", + "xterm": "4.11.0-beta.2", + "xterm-addon-search": "0.8.0", "xterm-addon-unicode11": "0.3.0-beta.3", - "xterm-addon-webgl": "0.10.0-beta.1" + "xterm-addon-webgl": "0.10.0-beta.2" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 87520c8459..fbedd38fb9 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -393,22 +393,22 @@ xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -xterm-addon-search@0.8.0-beta.3: - version "0.8.0-beta.3" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0-beta.3.tgz#c6c7e36a03706bd43d8bba383511acf9e435aed0" - integrity sha512-EZP97KJIJ4KGQaOPYiiOaRRJst6LOgeEFoQL46WcBl5EWH9pH8qfrv0BHAJ8+6nBV2B9u5M6rzxO1GvLLec19w== +xterm-addon-search@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881" + integrity sha512-MPJGPVPpHRUw9cLIuqQbrVepmENMOybVUSxIALz5h1ryyQBrVqVujq2hL5aroX5/dZJoHx9lGHQTVLQ07SKgKA== xterm-addon-unicode11@0.3.0-beta.3: version "0.3.0-beta.3" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.3.tgz#70af2dfb67809258edb62c19e2861f7ce5ccf5cd" integrity sha512-vaYopnOjn19wCLDCyIWPWLwKR7CvLPxB5YZ3CAxt9qL05o3symxIJJJC0DuCa4GaGKVjNc7EmjRCs5bsJ2O1tw== -xterm-addon-webgl@0.10.0-beta.1: - version "0.10.0-beta.1" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.1.tgz#e0bf964945a9aa8fc18318ddbd32e56ec99c219e" - integrity sha512-XNZMrmiyFaz3XiPq+LqF0qn2QHpUEwuk+cG53JwpJHnWo3dd2jxoIgHFQUcrnvHIVPZMbTKySIwLCCC9uQVl7Q== +xterm-addon-webgl@0.10.0-beta.2: + version "0.10.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.2.tgz#520547b2f845b2f9265f1817140b0f4e114c4a55" + integrity sha512-DLfmF5+H1M/0BABEaqJlLUasKck7TjyRWVlzGflFTWVCr7/Kqaf0al4KMlw3yWX76A1ITGXrWj0qbDn2Sixl2Q== -xterm@4.10.0-beta.4: - version "4.10.0-beta.4" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.10.0-beta.4.tgz#95efce7a40ec582101ec9777f4ccc6e68e95185e" - integrity sha512-q/yRy2nn4mp1jWZe218TJwlKjXCIr6h28Kw0JMB+lcTeU+MebZ3TrHqlrNVnB+UJfFDOpkw0qfKYfRoV8G/hXA== +xterm@4.11.0-beta.2: + version "4.11.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.11.0-beta.2.tgz#a95560b61c771f54a336c2eb10e1472e556d9f4b" + integrity sha512-0BUaAfuclnowirdOuB13OGgq6OUGg/8etnRVT6apgnOrLGOLRCE1NiL3KhxotleAf4gVP0m3iCxsIr3csDY40g== diff --git a/remote/yarn.lock b/remote/yarn.lock index a4facba843..f505b91b83 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -171,10 +171,10 @@ chartjs-color@^2.1.0: chartjs-color-string "^0.6.0" color-convert "^1.9.3" -chokidar@3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== +chokidar@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -184,7 +184,7 @@ chokidar@3.4.3: normalize-path "~3.0.0" readdirp "~3.5.0" optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.1" color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" @@ -327,10 +327,10 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== glob-parent@~5.1.0: version "5.1.0" @@ -544,15 +544,15 @@ ng2-charts@^1.6.0: dependencies: chart.js "^2.6.0" -node-addon-api@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.6.2.tgz#d8aad9781a5cfc4132cc2fecdbdd982534265217" - integrity sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA== +node-addon-api@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" + integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== -node-pty@0.10.0-beta17: - version "0.10.0-beta17" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0-beta17.tgz#962d4a3f4dc6772385e0cad529c209cef3bc79e6" - integrity sha512-tn7EANQacnAvnOQCImvgag1DL0tVmUoY/1yIZbh3u/BBpvCcGHLZJNn7TXheodRLr6hmGSUS2VbfcUr9p0gOug== +node-pty@0.10.0-beta19: + version "0.10.0-beta19" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0-beta19.tgz#b7cbfba53f7b2a816efe8c9302dd083cc5874458" + integrity sha512-4UIOGMvpofUbe+ZniBUtY8zc/psMURSzbMonQgIhK7JlMQsUwcbkDIrKzStVLJX0FkeZpUNlsVtK7qqzHvrUZA== dependencies: nan "^2.14.0" @@ -804,12 +804,12 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== -vscode-windows-ca-certs@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/vscode-windows-ca-certs/-/vscode-windows-ca-certs-0.2.0.tgz#086f0f4de57e2760a35ac6920831bff246237115" - integrity sha512-YBrJRT0zos+Yb1Qdn73GD8QZr7pa2IE96b5Y1hmmp6XeR8aYB7Iiq5gDAF/+/AxL+caSR9KPZQ6jiYWh5biD7w== +vscode-windows-ca-certs@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/vscode-windows-ca-certs/-/vscode-windows-ca-certs-0.3.0.tgz#324e1f8ba842bbf048a39e7c0ee8fe655e9adfcc" + integrity sha512-CYrpCEKmAFQJoZNReOrelNL+VKyebOVRCqL9evrBlVcpWQDliliJgU5RggGS8FPGtQ3jAKLQt9frF0qlxYYPKA== dependencies: - node-addon-api "1.6.2" + node-addon-api "^3.0.2" vscode-windows-registry@1.0.2: version "1.0.2" @@ -821,25 +821,25 @@ xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -xterm-addon-search@0.8.0-beta.3: - version "0.8.0-beta.3" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0-beta.3.tgz#c6c7e36a03706bd43d8bba383511acf9e435aed0" - integrity sha512-EZP97KJIJ4KGQaOPYiiOaRRJst6LOgeEFoQL46WcBl5EWH9pH8qfrv0BHAJ8+6nBV2B9u5M6rzxO1GvLLec19w== +xterm-addon-search@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881" + integrity sha512-MPJGPVPpHRUw9cLIuqQbrVepmENMOybVUSxIALz5h1ryyQBrVqVujq2hL5aroX5/dZJoHx9lGHQTVLQ07SKgKA== xterm-addon-unicode11@0.3.0-beta.3: version "0.3.0-beta.3" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.3.tgz#70af2dfb67809258edb62c19e2861f7ce5ccf5cd" integrity sha512-vaYopnOjn19wCLDCyIWPWLwKR7CvLPxB5YZ3CAxt9qL05o3symxIJJJC0DuCa4GaGKVjNc7EmjRCs5bsJ2O1tw== -xterm-addon-webgl@0.10.0-beta.1: - version "0.10.0-beta.1" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.1.tgz#e0bf964945a9aa8fc18318ddbd32e56ec99c219e" - integrity sha512-XNZMrmiyFaz3XiPq+LqF0qn2QHpUEwuk+cG53JwpJHnWo3dd2jxoIgHFQUcrnvHIVPZMbTKySIwLCCC9uQVl7Q== +xterm-addon-webgl@0.10.0-beta.2: + version "0.10.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.2.tgz#520547b2f845b2f9265f1817140b0f4e114c4a55" + integrity sha512-DLfmF5+H1M/0BABEaqJlLUasKck7TjyRWVlzGflFTWVCr7/Kqaf0al4KMlw3yWX76A1ITGXrWj0qbDn2Sixl2Q== -xterm@4.10.0-beta.4: - version "4.10.0-beta.4" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.10.0-beta.4.tgz#95efce7a40ec582101ec9777f4ccc6e68e95185e" - integrity sha512-q/yRy2nn4mp1jWZe218TJwlKjXCIr6h28Kw0JMB+lcTeU+MebZ3TrHqlrNVnB+UJfFDOpkw0qfKYfRoV8G/hXA== +xterm@4.11.0-beta.2: + version "4.11.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.11.0-beta.2.tgz#a95560b61c771f54a336c2eb10e1472e556d9f4b" + integrity sha512-0BUaAfuclnowirdOuB13OGgq6OUGg/8etnRVT6apgnOrLGOLRCE1NiL3KhxotleAf4gVP0m3iCxsIr3csDY40g== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/completions/zsh/_code b/resources/completions/zsh/_code index c7abee1bc5..087ea61f56 100644 --- a/resources/completions/zsh/_code +++ b/resources/completions/zsh/_code @@ -17,8 +17,8 @@ arguments=( '--telemetry[show all telemetry events which VS code collects]' '--extensions-dir[set the root path for extensions]:root path:_directories' '--list-extensions[list the installed extensions]' - '--category[filters installed extension list by category, when using --list-extension]' - '--show-versions[show versions of installed extensions, when using --list-extension]' + '--category[filters installed extension list by category, when using --list-extensions]' + '--show-versions[show versions of installed extensions, when using --list-extensions]' '--install-extension[install an extension]:id or path:_files -g "*.vsix(-.)"' '--uninstall-extension[uninstall an extension]:id or path:_files -g "*.vsix(-.)"' '--enable-proposed-api[enables proposed API features for extensions]::extension id: ' diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index 2c5ed24a8f..24a59f37f3 100644 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -66,13 +66,16 @@ NdCFTW7wY0Fb1fWJ+/KTsC4= if [ ! -f $CODE_SOURCE_PART ]; then # Write source list if it does not exist WRITE_SOURCE=1 - elif grep -q "# disabled on upgrade to" /etc/apt/sources.list.d/vscode.list; then + elif grep -Eq "http:\/\/packages\.microsoft\.com\/repos\/vscode" $CODE_SOURCE_PART; then + # Migrate from old repository + WRITE_SOURCE=1 + elif grep -q "# disabled on upgrade to" $CODE_SOURCE_PART; then # Write source list if it was disabled by OS upgrade WRITE_SOURCE=1 fi if [ "$WRITE_SOURCE" -eq "1" ]; then echo "### THIS FILE IS AUTOMATICALLY CONFIGURED ### # You may comment out this entry, but any other modifications may be lost. -deb [arch=amd64] http://packages.microsoft.com/repos/vscode stable main" > $CODE_SOURCE_PART +deb [arch=amd64,arm64,armhf] http://packages.microsoft.com/repos/code stable main" > $CODE_SOURCE_PART fi fi diff --git a/resources/web/code-web.js b/resources/web/code-web.js index d3fb19227d..5621bd3bc8 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -21,6 +21,7 @@ const vfs = require('vinyl-fs'); const uuid = require('uuid'); const extensions = require('../../build/lib/extensions'); +const { getBuiltInExtensions } = require('../../build/lib/builtInExtensions'); const APP_ROOT = path.join(__dirname, '..', '..'); const BUILTIN_EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); @@ -28,6 +29,14 @@ const BUILTIN_MARKETPLACE_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'built const WEB_DEV_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInWebDevExtensions'); const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html'); +// This is useful to simulate real world CORS +const ALLOWED_CORS_ORIGINS = [ + 'http://localhost:8081', + 'http://127.0.0.1:8081', + 'http://localhost:8080', + 'http://127.0.0.1:8080', +]; + const WEB_PLAYGROUND_VERSION = '0.0.10'; const args = minimist(process.argv, { @@ -37,7 +46,6 @@ const args = minimist(process.argv, { 'verbose', 'wrap-iframe', 'enable-sync', - 'trusted-types' ], string: [ 'scheme', @@ -54,7 +62,6 @@ if (args.help) { 'yarn web [options]\n' + ' --no-launch Do not open VSCode web in the browser\n' + ' --wrap-iframe Wrap the Web Worker Extension Host in an iframe\n' + - ' --trusted-types Enable trusted types (report only)\n' + ' --enable-sync Enable sync by default\n' + ' --scheme Protocol (https or http)\n' + ' --host Remote host\n' + @@ -82,6 +89,8 @@ const exists = (path) => util.promisify(fs.exists)(path); const readFile = (path) => util.promisify(fs.readFile)(path); async function getBuiltInExtensionInfos() { + await getBuiltInExtensions(); + const allExtensions = []; /** @type {Object.} */ const locations = {}; @@ -281,6 +290,17 @@ secondaryServer.on('error', err => { console.error(err); }); +/** + * @param {import('http').IncomingMessage} req + */ +function addCORSReplyHeader(req) { + if (typeof req.headers['origin'] !== 'string') { + // not a CORS request + return false; + } + return (ALLOWED_CORS_ORIGINS.indexOf(req.headers['origin']) >= 0); +} + /** * @param {import('http').IncomingMessage} req * @param {import('http').ServerResponse} res @@ -291,9 +311,10 @@ async function handleStatic(req, res, parsedUrl) { if (/^\/static\/extensions\//.test(parsedUrl.pathname)) { const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/static/extensions/'.length)); const filePath = getExtensionFilePath(relativePath, (await builtInExtensionsPromise).locations); - const responseHeaders = { - 'Access-Control-Allow-Origin': '*' - }; + const responseHeaders = {}; + if (addCORSReplyHeader(req)) { + responseHeaders['Access-Control-Allow-Origin'] = '*'; + } if (!filePath) { return serveError(req, res, 400, `Bad request.`, responseHeaders); } @@ -315,9 +336,10 @@ async function handleExtension(req, res, parsedUrl) { // Strip `/extension/` from the path const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/extension/'.length)); const filePath = getExtensionFilePath(relativePath, (await commandlineProvidedExtensionsPromise).locations); - const responseHeaders = { - 'Access-Control-Allow-Origin': '*' - }; + const responseHeaders = {}; + if (addCORSReplyHeader(req)) { + responseHeaders['Access-Control-Allow-Origin'] = '*'; + } if (!filePath) { return serveError(req, res, 400, `Bad request.`, responseHeaders); } @@ -377,11 +399,18 @@ async function handleRoot(req, res) { fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`); } + const secondaryHost = ( + req.headers['host'] + ? req.headers['host'].replace(':' + PORT, ':' + SECONDARY_PORT) + : `${HOST}:${SECONDARY_PORT}` + ); const webConfigJSON = { folderUri: folderUri, staticExtensions, - enableSyncByDefault: args['enable-sync'], - webWorkerExtensionHostIframeSrc: `${SCHEME}://${HOST}:${SECONDARY_PORT}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` + settingsSyncOptions: { + enabled: args['enable-sync'] + }, + webWorkerExtensionHostIframeSrc: `${SCHEME}://${secondaryHost}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` }; if (args['wrap-iframe']) { webConfigJSON._wrapWebWorkerExtHostInIframe = true; @@ -419,12 +448,10 @@ async function handleRoot(req, res) { .replace('{{WORKBENCH_CREDENTIALS}}', () => escapeAttribute(JSON.stringify(credentials))) .replace('{{WEBVIEW_ENDPOINT}}', ''); - - const headers = { 'Content-Type': 'text/html' }; - if (args['trusted-types']) { - headers['Content-Security-Policy-Report-Only'] = 'require-trusted-types-for \'script\';'; - } - + const headers = { + 'Content-Type': 'text/html', + 'Content-Security-Policy': 'require-trusted-types-for \'script\';' + }; res.writeHead(200, headers); return res.end(data); } diff --git a/scripts/test-extensions-unit.bat b/scripts/test-extensions-unit.bat index e0f3d80824..5ebb000e58 100755 --- a/scripts/test-extensions-unit.bat +++ b/scripts/test-extensions-unit.bat @@ -1,37 +1,50 @@ :: Runs Extension tests +@echo off setlocal pushd %~dp0\.. -set VSCODEUSERDATADIR=%TMP%\adsuser-%RANDOM%-%TIME:~6,5% -set VSCODEEXTENSIONSDIR=%TMP%\adsext-%RANDOM%-%TIME:~6,5% -echo %VSCODEUSERDATADIR% -echo %VSCODEEXTENSIONSDIR% +set VSCODEUSERDATADIR=%TMP%\adsuser_%RANDOM%-%TIME:~6,5% +set VSCODEEXTENSIONSDIR=%TMP%\adsext_%RANDOM%-%TIME:~6,5% +echo VSCODEUSERDATADIR : '%VSCODEUSERDATADIR%' +echo VSCODEEXTENSIONSDIR : '%VSCODEEXTENSIONSDIR%' +set VSCODECRASHDIR=%~dp0\..\.build\crashes :: Figure out which Electron to use for running tests if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Run out of sources: no need to compile as code.bat takes care of it + chcp 65001 set INTEGRATION_TEST_ELECTRON_PATH=.\scripts\code.bat + set VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE=1 - echo "Running unit tests out of sources." + echo Storing crash reports into '%VSCODECRASHDIR%'. + echo Running unit tests out of sources. ) else ( - :: Run from a build: need to compile all test extensions - call yarn gulp compile-extension:admin-tool-ext-win - call yarn gulp compile-extension:agent - call yarn gulp compile-extension:arc - call yarn gulp compile-extension:azurecore - call yarn gulp compile-extension:cms - call yarn gulp compile-extension:dacpac - call yarn gulp compile-extension:import - call yarn gulp compile-extension:schema-compare - call yarn gulp compile-extension:mssql - call yarn gulp compile-extension:notebook - call yarn gulp compile-extension:resource-deployment - call yarn gulp compile-extension:machine-learning - call yarn gulp compile-extension:sql-database-projects + :: Run from a built: need to compile all test extensions + :: because we run extension tests from their source folders + :: and the build bundles extensions into .build webpacked + :: {{SQL CARBON EDIT}} Don't compile unused extensions + call yarn gulp compile-extension:admin-tool-ext-win^ + compile-extension:agent^ + compile-extension:arc^ + compile-extension:azurecore^ + compile-extension:cms^ + compile-extension:dacpac^ + compile-extension:import^ + compile-extension:schema-compare^ + compile-extension:machine-learning^ + compile-extension:mssql^ + compile-extension:notebook^ + compile-extension:resource-deployment^ + compile-extension:sql-database-projects - echo "Running unit tests with '%INTEGRATION_TEST_ELECTRON_PATH%' as build." + :: Configuration for more verbose output + set VSCODE_CLI=1 + set ELECTRON_ENABLE_LOGGING=1 + + echo Storing crash reports into '%VSCODECRASHDIR%'. + echo Running unit tests with '%INTEGRATION_TEST_ELECTRON_PATH%' as build. ) :: Default to only running stable tests if test grep isn't set @@ -41,82 +54,82 @@ if "%ADS_TEST_GREP%" == "" ( SET ADS_TEST_INVERT_GREP=1 ) -@echo OFF +set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-keytar --user-data-dir=%VSCODEUSERDATADIR% --remote-debugging-port=9222 --extensions-dir=%VSCODEEXTENSIONSDIR% echo *************************************************** echo *** starting admin tool extension windows tests *** echo *************************************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\admin-tool-ext-win --extensionTestsPath=%~dp0\..\extensions\admin-tool-ext-win\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\admin-tool-ext-win --extensionTestsPath=%~dp0\..\extensions\admin-tool-ext-win\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo **************************** echo *** starting agent tests *** echo **************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\agent --extensionTestsPath=%~dp0\..\extensions\agent\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\agent --extensionTestsPath=%~dp0\..\extensions\agent\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ************************** echo *** starting arc tests *** echo ************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\arc --extensionTestsPath=%~dp0\..\extensions\arc\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\arc --extensionTestsPath=%~dp0\..\extensions\arc\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ***************************** echo *** starting azdata tests *** echo ***************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\azdata --extensionTestsPath=%~dp0\..\extensions\azdata\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\azdata --extensionTestsPath=%~dp0\..\extensions\azdata\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ******************************** echo *** starting azurecore tests *** echo ******************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ************************** echo *** starting cms tests *** echo ************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\cms --extensionTestsPath=%~dp0\..\extensions\cms\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\cms --extensionTestsPath=%~dp0\..\extensions\cms\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ***************************** echo *** starting dacpac tests *** echo ***************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\dacpac --extensionTestsPath=%~dp0\..\extensions\dacpac\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\dacpac --extensionTestsPath=%~dp0\..\extensions\dacpac\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ***************************** echo *** starting import tests *** echo ***************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\import --extensionTestsPath=%~dp0\..\extensions\import\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\import --extensionTestsPath=%~dp0\..\extensions\import\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ************************************* echo *** starting schema compare tests *** echo ************************************* -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\schema-compare --extensionTestsPath=%~dp0\..\extensions\schema-compare\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\schema-compare --extensionTestsPath=%~dp0\..\extensions\schema-compare\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ******************************* echo *** starting notebook tests *** echo ******************************* -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\notebook --extensionTestsPath=%~dp0\..\extensions\notebook\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\notebook --extensionTestsPath=%~dp0\..\extensions\notebook\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ****************************************** echo *** starting resource deployment tests *** echo ****************************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\resource-deployment --extensionTestsPath=%~dp0\..\extensions\resource-deployment\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\resource-deployment --extensionTestsPath=%~dp0\..\extensions\resource-deployment\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ******************************* echo *** starting machine-learning tests *** echo ******************************* -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\machine-learning --extensionTestsPath=%~dp0\..\extensions\machine-learning\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\machine-learning --extensionTestsPath=%~dp0\..\extensions\machine-learning\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% REM echo ****************************************** REM echo *** starting mssql tests *** REM echo ****************************************** -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\mssql --extensionTestsPath=%~dp0\..\extensions\mssql\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +REM call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\mssql --extensionTestsPath=%~dp0\..\extensions\mssql\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ******************************************** echo *** starting sql-database-projects tests *** echo ******************************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\sql-database-projects --extensionTestsPath=%~dp0\..\extensions\sql-database-projects\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\sql-database-projects --extensionTestsPath=%~dp0\..\extensions\sql-database-projects\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% echo ******************************************** echo *** starting data-workspace tests *** echo ******************************************** -call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\data-workspace --extensionTestsPath=%~dp0\..\extensions\data-workspace\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\data-workspace --extensionTestsPath=%~dp0\..\extensions\data-workspace\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/scripts/test-extensions-unit.js b/scripts/test-extensions-unit.js index b7394168af..a9ff946c40 100644 --- a/scripts/test-extensions-unit.js +++ b/scripts/test-extensions-unit.js @@ -33,6 +33,12 @@ let argv = require('yargs') .default('extensions', extensionList) .strict().help().wrap(null).version(false).argv; +let LINUX_EXTRA_ARGS=''; + +if(os.platform() === 'linux') { + LINUX_EXTRA_ARGS = '--no-sandbox --disable-dev-shm-usage --use-gl=swiftshader'; +} + if (!process.env.INTEGRATION_TEST_ELECTRON_PATH) { process.env.INTEGRATION_TEST_ELECTRON_PATH = path.join(__dirname, '..', 'scripts', os.platform() === 'win32' ? 'code.bat' : 'code.sh'); console.log('Running unit tests out of sources.'); @@ -65,9 +71,14 @@ for (const ext of argv.extensions) { console.log(`VSCODEUSERDATADIR : ${VSCODEUSERDATADIR}`); console.log(`VSCODEEXTENSIONSDIR : ${VSCODEEXTENSIONSDIR}`); - const command = `${process.env.INTEGRATION_TEST_ELECTRON_PATH} --no-sandbox --extensionDevelopmentPath=${path.join(__dirname, '..', 'extensions', ext)} --extensionTestsPath=${path.join(__dirname, '..', 'extensions', ext, 'out', 'test')} --user-data-dir=${VSCODEUSERDATADIR} --extensions-dir=${VSCODEEXTENSIONSDIR} --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu`; + const command = `${process.env.INTEGRATION_TEST_ELECTRON_PATH} ${LINUX_EXTRA_ARGS} --extensionDevelopmentPath=${path.join(__dirname, '..', 'extensions', ext)} --extensionTestsPath=${path.join(__dirname, '..', 'extensions', ext, 'out', 'test')} --user-data-dir=${VSCODEUSERDATADIR} --extensions-dir=${VSCODEEXTENSIONSDIR} --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --no-cached-data --disable-keytar`; console.log(`Command used: ${command}`); - console.log(execSync(command, { stdio: 'inherit' })); + const env = { + VSCODE_CLI: 1, + ELECTRON_ENABLE_STACK_DUMPING: 1, + ELECTRON_ENABLE_LOGGING: 1 + }; + console.log(execSync(command, { stdio: 'inherit', env: env})); // clean up if (!process.env.NO_CLEANUP) { diff --git a/scripts/test-extensions-unit.sh b/scripts/test-extensions-unit.sh index dd5f52374c..a8a280d974 100755 --- a/scripts/test-extensions-unit.sh +++ b/scripts/test-extensions-unit.sh @@ -1,20 +1,20 @@ -#!/bin/bash - -# Runs Extension Tests +#!/usr/bin/env bash set -e if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } ROOT=$(dirname $(dirname $(realpath "$0"))) - VSCODEUSERDATADIR=`mktemp -d -t 'myuserdatadir'` - VSCODEEXTDIR=`mktemp -d -t 'myextdir'` else ROOT=$(dirname $(dirname $(readlink -f $0))) - VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` - VSCODEEXTDIR=`mktemp -d 2>/dev/null` - LINUX_NO_SANDBOX="--no-sandbox" # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + LINUX_EXTRA_ARGS="--no-sandbox" fi +VSCODEUSERDATADIR=`mktemp -d -t adsuser_XXXXXXXXXX 2>/dev/null` +VSCODEEXTDIR=`mktemp -d -t adsext_XXXXXXXXXX 2>/dev/null` +VSCODECRASHDIR=$ROOT/.build/crashes +cd $ROOT + # Default to only running stable tests if test grep isn't set if [[ "$ADS_TEST_GREP" == "" ]]; then echo Running stable tests only @@ -35,84 +35,134 @@ else echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." fi +# Figure out which Electron to use for running tests +if [ -z "$INTEGRATION_TEST_ELECTRON_PATH" ] +then + # Run out of sources: no need to compile as code.sh takes care of it + INTEGRATION_TEST_ELECTRON_PATH="./scripts/code.sh" + + echo "Storing crash reports into '$VSCODECRASHDIR'." + echo "Running integration tests out of sources." +else + # Run from a built: need to compile all test extensions + # because we run extension tests from their source folders + # and the build bundles extensions into .build webpacked + yarn gulp compile-extension:admin-tool-ext-win \ + compile-extension:agent \ + compile-extension:arc \ + compile-extension:azurecore \ + compile-extension:cms \ + compile-extension:dacpac \ + compile-extension:import \ + compile-extension:schema-compare \ + compile-extension:machine-learning \ + compile-extension:mssql \ + compile-extension:notebook \ + compile-extension:resource-deployment \ + compile-extension:sql-database-projects + + # Configuration for more verbose output + export VSCODE_CLI=1 + export ELECTRON_ENABLE_STACK_DUMPING=1 + export ELECTRON_ENABLE_LOGGING=1 + + # Production builds are run on docker containers where size of /dev/shm partition < 64MB which causes OOM failure + # for chromium compositor that uses the partition for shared memory + if [ "$LINUX_EXTRA_ARGS" ] + then + LINUX_EXTRA_ARGS="$LINUX_EXTRA_ARGS --disable-dev-shm-usage --use-gl=swiftshader" + fi + + echo "Storing crash reports into '$VSCODECRASHDIR'." + echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." +fi + +if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then + after_suite() { true; } +else + after_suite() { killall $INTEGRATION_TEST_APP_NAME || true; } +fi + cd $ROOT -echo $VSCODEUSERDATADIR -echo $VSCODEEXTDIR +echo "VSCODEUSERDATADIR : '$VSCODEUSERDATADIR'" +echo "VSCODEEXTDIR : '$VSCODEEXTDIR'" + +ALL_PLATFORMS_API_TESTS_EXTRA_ARGS="--disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-keytar --disable-extensions --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR" echo *************************************************** echo *** starting admin tool extension windows tests *** echo *************************************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/admin-tool-ext-win --extensionTestsPath=$ROOT/extensions/admin-tool-ext-win/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/admin-tool-ext-win --extensionTestsPath=$ROOT/extensions/admin-tool-ext-win/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo **************************** echo *** starting agent tests *** echo **************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/agent --extensionTestsPath=$ROOT/extensions/agent/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/agent --extensionTestsPath=$ROOT/extensions/agent/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ************************** echo *** starting arc tests *** echo ************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/arc --extensionTestsPath=$ROOT/extensions/arc/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/arc --extensionTestsPath=$ROOT/extensions/arc/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ***************************** echo *** starting azdata tests *** echo ***************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/azdata --extensionTestsPath=$ROOT/extensions/azdata/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/azdata --extensionTestsPath=$ROOT/extensions/azdata/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ******************************** echo *** starting azurecore tests *** echo ******************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ************************** echo *** starting cms tests *** echo ************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/cms --extensionTestsPath=$ROOT/extensions/cms/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/cms --extensionTestsPath=$ROOT/extensions/cms/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ***************************** echo *** starting dacpac tests *** echo ***************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/dacpac --extensionTestsPath=$ROOT/extensions/dacpac/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/dacpac --extensionTestsPath=$ROOT/extensions/dacpac/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ***************************** echo *** starting import tests *** echo ***************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/import --extensionTestsPath=$ROOT/extensions/import/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/import --extensionTestsPath=$ROOT/extensions/import/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ************************************* echo *** starting schema compare tests *** echo ************************************* -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/schema-compare --extensionTestsPath=$ROOT/extensions/schema-compare/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/schema-compare --extensionTestsPath=$ROOT/extensions/schema-compare/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ******************************* echo *** starting notebook tests *** echo ******************************* -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/notebook --extensionTestsPath=$ROOT/extensions/notebook/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/notebook --extensionTestsPath=$ROOT/extensions/notebook/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ****************************************** echo *** starting resource deployment tests *** echo ****************************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/resource-deployment --extensionTestsPath=$ROOT/extensions/resource-deployment/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/resource-deployment --extensionTestsPath=$ROOT/extensions/resource-deployment/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ************************************************ echo *** starting machine-learning tests *** echo ************************************************ -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/machine-learning --extensionTestsPath=$ROOT/extensions/machine-learning/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/machine-learning --extensionTestsPath=$ROOT/extensions/machine-learning/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS # echo ****************************************** # echo *** starting mssql tests *** # echo ****************************************** -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/mssql --extensionTestsPath=$ROOT/extensions/mssql/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/mssql --extensionTestsPath=$ROOT/extensions/mssql/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ******************************************** echo *** starting sql-database-projects tests *** echo ******************************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/sql-database-projects --extensionTestsPath=$ROOT/extensions/sql-database-projects/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/sql-database-projects --extensionTestsPath=$ROOT/extensions/sql-database-projects/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS echo ******************************************** echo *** starting data-workspace tests *** echo ******************************************** -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/data-workspace --extensionTestsPath=$ROOT/extensions/data-workspace/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --extensionDevelopmentPath=$ROOT/extensions/data-workspace --extensionTestsPath=$ROOT/extensions/data-workspace/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS if [[ "$NO_CLEANUP" == "" ]]; then rm -r $VSCODEUSERDATADIR diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index a5a74146d8..c68e94a9bb 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -19,8 +19,19 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Run from a built: need to compile all test extensions :: because we run extension tests from their source folders :: and the build bundles extensions into .build webpacked + :: {{SQL CARBON EDIT}} Don't compile unused extensions call yarn gulp compile-extension:azurecore^ compile-extension:git + :: compile-extension:vscode-api-tests^ + :: compile-extension:vscode-colorize-tests^ + :: compile-extension:markdown-language-features^ + :: compile-extension:typescript-language-features^ + :: compile-extension:vscode-custom-editor-tests^ + :: compile-extension:vscode-notebook-tests^ + :: compile-extension:emmet^ + :: compile-extension:css-language-features-server^ + :: compile-extension:html-language-features-server^ + :: compile-extension:json-language-features-server^ :: Configuration for more verbose output set VSCODE_CLI=1 @@ -31,48 +42,50 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( ) :: Integration & performance tests in AMD -:: TODO port over an re-enable API tests :: call .\scripts\test.bat --runGlob **\*.integrationTest.js %* :: if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in the extension host -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% +set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-keytar --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: {{SQL CARBON EDIT}} Don't run tests for unused extensions +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +:: if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +:: if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\typescript-language-features --extensionTestsPath=%~dp0\..\extensions\typescript-language-features\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +:: if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\markdown-language-features\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\typescript-language-features --extensionTestsPath=%~dp0\..\extensions\typescript-language-features\out\test\unit %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +:: if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\markdown-language-features\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +:: if %errorlevel% neq 0 exit /b %errorlevel% -call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --no-cached-data --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% . +:: if %errorlevel% neq 0 exit /b %errorlevel% + +:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% +:: if %errorlevel% neq 0 exit /b %errorlevel% + +call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% -REM if %errorlevel% neq 0 exit /b %errorlevel% - for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% mkdir %GITWORKSPACE% -call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --enable-proposed-api=vscode.git --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --enable-proposed-api=vscode.git %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in commonJS (CSS, HTML) -REM call %~dp0\node-electron.bat %~dp0\..\extensions\css-language-features/server/test/index.js -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: call %~dp0\node-electron.bat %~dp0\..\extensions\css-language-features/server/test/index.js +:: if %errorlevel% neq 0 exit /b %errorlevel% -REM call %~dp0\node-electron.bat %~dp0\..\extensions\html-language-features/server/test/index.js -REM if %errorlevel% neq 0 exit /b %errorlevel% +:: call %~dp0\node-electron.bat %~dp0\..\extensions\html-language-features/server/test/index.js +:: if %errorlevel% neq 0 exit /b %errorlevel% rmdir /s /q %VSCODEUSERDATADIR% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 4b6ee7688b..42b8afd87e 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -6,7 +6,8 @@ if [[ "$OSTYPE" == "darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) - LINUX_NO_SANDBOX="--no-sandbox" # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + LINUX_EXTRA_ARGS="--no-sandbox" fi VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` @@ -25,35 +26,84 @@ else # Run from a built: need to compile all test extensions # because we run extension tests from their source folders # and the build bundles extensions into .build webpacked + # {{SQL CARBON EDIT}} Don't compile unused extensions yarn gulp compile-extension:azurecore \ compile-extension:git + # compile-extension:vscode-api-tests \ + # compile-extension:vscode-colorize-tests \ + # compile-extension:vscode-custom-editor-tests \ + # compile-extension:vscode-notebook-tests \ + # compile-extension:markdown-language-features \ + # compile-extension:typescript-language-features \ + # compile-extension:emmet \ + # compile-extension:css-language-features-server \ + # compile-extension:html-language-features-server \ + # compile-extension:json-language-features-server \ + # Configuration for more verbose output export VSCODE_CLI=1 export ELECTRON_ENABLE_STACK_DUMPING=1 export ELECTRON_ENABLE_LOGGING=1 + # Production builds are run on docker containers where size of /dev/shm partition < 64MB which causes OOM failure + # for chromium compositor that uses the partition for shared memory + if [ "$LINUX_EXTRA_ARGS" ] + then + LINUX_EXTRA_ARGS="$LINUX_EXTRA_ARGS --disable-dev-shm-usage --use-gl=swiftshader" + fi + echo "Storing crash reports into '$VSCODECRASHDIR'." echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." fi +if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then + after_suite() { true; } +else + after_suite() { killall $INTEGRATION_TEST_APP_NAME || true; } +fi + # Integration tests in AMD ./scripts/test.sh --runGlob **/*.integrationTest.js "$@" +after_suite # Tests in the extension host -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/markdown-language-features/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --no-cached-data --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +ALL_PLATFORMS_API_TESTS_EXTRA_ARGS="--disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-keytar --disable-extensions --user-data-dir=$VSCODEUSERDATADIR" + +# {{SQL CARBON EDIT}} Don't run tests for unused extensions +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/markdown-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/emmet/test-workspace --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +after_suite + +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +# after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test $ALL_PLATFORMS_API_TESTS_EXTRA_ARGS +after_suite # Tests in commonJS (CSS, HTML) # cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js +# after_suite + # cd $ROOT/extensions/html-language-features/server && $ROOT/scripts/node-electron.sh test/index.js +# after_suite rm -rf $VSCODEUSERDATADIR diff --git a/scripts/test.sh b/scripts/test.sh index 82a0aad4d3..7830c232b9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,6 +7,8 @@ if [[ "$OSTYPE" == "darwin"* ]] || [[ "$AGENT_OS" == "Darwin"* ]]; then ROOT=$(dirname $(dirname $(realpath "$0"))) else ROOT=$(dirname $(dirname $(readlink -f $0))) + # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + LINUX_EXTRA_ARGS="--no-sandbox --disable-dev-shm-usage --use-gl=swiftshader" fi cd $ROOT @@ -42,5 +44,5 @@ else cd $ROOT ; \ ELECTRON_ENABLE_LOGGING=1 \ "$CODE" \ - test/unit/electron/index.js --no-sandbox "$@" # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + test/unit/electron/index.js $LINUX_EXTRA_ARGS "$@" fi diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index dc1cbfa9d8..6aaa5704d1 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -8,6 +8,7 @@ const loader = require('./vs/loader'); const bootstrap = require('./bootstrap'); +const performance = require('./vs/base/common/performance'); // Bootstrap: NLS const nlsConfig = bootstrap.setupNLS(); @@ -55,5 +56,6 @@ exports.load = function (entrypoint, onLoad, onError) { onLoad = onLoad || function () { }; onError = onError || function (err) { console.error(err); }; + performance.mark(`code/fork/willLoadCode`); loader([entrypoint], onLoad, onError); }; diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 2a392319a8..eed559048d 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -6,6 +6,9 @@ //@ts-check 'use strict'; +const performance = require('./vs/base/common/performance'); +performance.mark('code/fork/start'); + const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); @@ -20,7 +23,7 @@ if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) { } // Configure: pipe logging to parent process -if (!!process.send && process.env.PIPE_LOGGING === 'true') { +if (!!process.send && process.env['VSCODE_PIPE_LOGGING'] === 'true') { pipeLoggingToParent(); } @@ -38,7 +41,7 @@ if (process.env['VSCODE_PARENT_PID']) { configureCrashReporter(); // Load AMD entry point -require('./bootstrap-amd').load(process.env['AMD_ENTRYPOINT']); +require('./bootstrap-amd').load(process.env['VSCODE_AMD_ENTRYPOINT']); //#region Helpers @@ -79,7 +82,7 @@ function pipeLoggingToParent() { // Add the stack trace as payload if we are told so. We remove the message and the 2 top frames // to start the stacktrace where the console message was being written - if (process.env.VSCODE_LOG_STACK === 'true') { + if (process.env['VSCODE_LOG_STACK'] === 'true') { const stack = new Error().stack; if (stack) { argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') }); @@ -135,18 +138,47 @@ function pipeLoggingToParent() { && !(obj instanceof Date); } + /** + * + * @param {'log' | 'warn' | 'error'} severity + * @param {string} args + */ + function safeSendConsoleMessage(severity, args) { + safeSend({ type: '__$console', severity, arguments: args }); + } + + /** + * @param {'log' | 'info' | 'warn' | 'error'} method + * @param {'log' | 'warn' | 'error'} severity + */ + function wrapConsoleMethod(method, severity) { + if (process.env['VSCODE_LOG_NATIVE'] === 'true') { + const original = console[method]; + console[method] = function () { + safeSendConsoleMessage(severity, safeToArray(arguments)); + + const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout; + stream.write('\nSTART_NATIVE_LOG\n'); + original.apply(console, arguments); + stream.write('\nEND_NATIVE_LOG\n'); + }; + } else { + console[method] = function () { safeSendConsoleMessage(severity, safeToArray(arguments)); }; + } + } + // Pass console logging to the outside so that we have it in the main side if told so - if (process.env.VERBOSE_LOGGING === 'true') { - console.log = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); }; - console.info = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); }; - console.warn = function () { safeSend({ type: '__$console', severity: 'warn', arguments: safeToArray(arguments) }); }; - } else { + if (process.env['VSCODE_VERBOSE_LOGGING'] === 'true') { + wrapConsoleMethod('info', 'log'); + wrapConsoleMethod('log', 'log'); + wrapConsoleMethod('warn', 'warn'); + wrapConsoleMethod('error', 'error'); + } else if (process.env['VSCODE_LOG_NATIVE'] !== 'true') { console.log = function () { /* ignore */ }; console.warn = function () { /* ignore */ }; console.info = function () { /* ignore */ }; + wrapConsoleMethod('error', 'error'); } - - console.error = function () { safeSend({ type: '__$console', severity: 'error', arguments: safeToArray(arguments) }); }; } function handleExceptions() { @@ -177,11 +209,11 @@ function terminateWhenParentTerminates() { } function configureCrashReporter() { - const crashReporterOptionsRaw = process.env['CRASH_REPORTER_START_OPTIONS']; + const crashReporterOptionsRaw = process.env['VSCODE_CRASH_REPORTER_START_OPTIONS']; if (typeof crashReporterOptionsRaw === 'string') { try { const crashReporterOptions = JSON.parse(crashReporterOptionsRaw); - if (crashReporterOptions) { + if (crashReporterOptions && process['crashReporter'] /* Electron only */) { process['crashReporter'].start(crashReporterOptions); } } catch (error) { diff --git a/src/bootstrap-node.js b/src/bootstrap-node.js index ec7949a33d..1c01a714c4 100644 --- a/src/bootstrap-node.js +++ b/src/bootstrap-node.js @@ -62,7 +62,7 @@ exports.removeGlobalNodeModuleLookupPaths = function () { /** * Helper to enable portable mode. * - * @param {{ portable?: string; applicationName: string; }} product + * @param {Partial} product * @returns {{ portableDataPath: string; isPortable: boolean; }} */ exports.configurePortable = function (product) { diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 1886080ef5..49c0a29bb9 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -27,6 +27,7 @@ const webFrame = preloadGlobals.webFrame; const safeProcess = preloadGlobals.process; const configuration = parseWindowConfiguration(); + const useCustomProtocol = sandbox || typeof safeProcess.env['ENABLE_VSCODE_BROWSER_CODE_LOADING'] === 'string'; // Start to resolve process.env before anything gets load // so that we can run loading and resolving in parallel @@ -34,7 +35,7 @@ /** * @param {string[]} modulePaths - * @param {(result, configuration: object) => any} resultCallback + * @param {(result: unknown, configuration: object) => Promise | undefined} resultCallback * @param {{ forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean, canModifyDOM?: (config: object) => void, beforeLoaderConfig?: (config: object, loaderConfig: object) => void, beforeRequire?: () => void }=} options */ function load(modulePaths, resultCallback, options) { @@ -83,31 +84,32 @@ window.document.documentElement.setAttribute('lang', locale); - // do not advertise AMD to avoid confusing UMD modules loaded with nodejs (TODO@sandbox non-sandboxed only) - if (!sandbox) { + // do not advertise AMD to avoid confusing UMD modules loaded with nodejs + if (!useCustomProtocol) { window['define'] = undefined; } // replace the patched electron fs with the original node fs for all AMD code (TODO@sandbox non-sandboxed only) if (!sandbox) { - require.define('fs', ['original-fs'], function (originalFS) { return originalFS; }); + require.define('fs', [], function () { return require.__$__nodeRequire('original-fs'); }); } window['MonacoEnvironment'] = {}; - const baseUrl = sandbox ? - `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'vscode-file', fallbackAuthority: 'vscode-app' })}/out` : + const baseUrl = useCustomProtocol ? + `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'azuredatastudio-file', fallbackAuthority: 'azuredatastudio-app' })}/out` : `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32' })}/out`; const loaderConfig = { baseUrl: baseUrl, 'vs/nls': nlsConfig, amdModulesPattern: /^(vs|sql)\//, // {{SQL CARBON EDIT}} include sql in regex - preferScriptTags: sandbox + preferScriptTags: useCustomProtocol }; + // use a trusted types policy when loading via script tags - if (loaderConfig.preferScriptTags && window && window.trustedTypes) { // {{SQL CARBON EDIT}} fix uglify error - loaderConfig.trustedTypesPolicy = window.trustedTypes.createPolicy('amdLoader', { + if (loaderConfig.preferScriptTags) { + loaderConfig.trustedTypesPolicy = window.trustedTypes?.createPolicy('amdLoader', { createScriptURL(value) { if (value.startsWith(window.location.origin)) { return value; @@ -117,6 +119,25 @@ }); } + // Enable loading of node modules: + // - sandbox: we list paths of webpacked modules to help the loader + // - non-sandbox: we signal that any module that does not begin with + // `vs/` should be loaded using node.js require() + if (sandbox) { + loaderConfig.paths = { + 'vscode-textmate': `../node_modules/vscode-textmate/release/main`, + 'vscode-oniguruma': `../node_modules/vscode-oniguruma/release/main`, + 'xterm': `../node_modules/xterm/lib/xterm.js`, + 'xterm-addon-search': `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, + 'xterm-addon-unicode11': `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, + 'xterm-addon-webgl': `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, + 'iconv-lite-umd': `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, + 'jschardet': `../node_modules/jschardet/dist/jschardet.min.js`, + }; + } else { + loaderConfig.amdModulesPattern = /^(vs|sql)\//; + } + // cached data config if (configuration.nodeCachedDataDir) { loaderConfig.nodeCachedData = { @@ -145,10 +166,9 @@ try { // Wait for process environment being fully resolved - const perf = perfLib(); - perf.mark('willWaitForShellEnv'); + performance.mark('code/willWaitForShellEnv'); await whenEnvResolved; - perf.mark('didWaitForShellEnv'); + performance.mark('code/didWaitForShellEnv'); // Callback only after process environment is resolved const callbackResult = resultCallback(result, configuration); @@ -166,7 +186,7 @@ } /** - * Parses the contents of the `INativeWindowConfiguration` that + * Parses the contents of the window condiguration that * is passed into the URL from the `electron-main` side. * * @returns {{ @@ -195,22 +215,26 @@ function registerDeveloperKeybindings(disallowReloadKeybinding) { const ipcRenderer = preloadGlobals.ipcRenderer; - const extractKey = function (e) { - return [ - e.ctrlKey ? 'ctrl-' : '', - e.metaKey ? 'meta-' : '', - e.altKey ? 'alt-' : '', - e.shiftKey ? 'shift-' : '', - e.keyCode - ].join(''); - }; + const extractKey = + /** + * @param {KeyboardEvent} e + */ + function (e) { + return [ + e.ctrlKey ? 'ctrl-' : '', + e.metaKey ? 'meta-' : '', + e.altKey ? 'alt-' : '', + e.shiftKey ? 'shift-' : '', + e.keyCode + ].join(''); + }; // Devtools & reload support const TOGGLE_DEV_TOOLS_KB = (safeProcess.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I const TOGGLE_DEV_TOOLS_KB_ALT = '123'; // F12 const RELOAD_KB = (safeProcess.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R - /** @type {((e: any) => void) | undefined} */ + /** @type {((e: KeyboardEvent) => void) | undefined} */ let listener = function (e) { const key = extractKey(e); if (key === TOGGLE_DEV_TOOLS_KB || key === TOGGLE_DEV_TOOLS_KB_ALT) { @@ -252,7 +276,7 @@ */ function bootstrap() { // @ts-ignore (defined in bootstrap.js) - return globalThis.MonacoBootstrap; + return window.MonacoBootstrap; } /** @@ -263,51 +287,8 @@ return window.vscode; } - /** - * TODO@sandbox this should not use the file:// protocol at all - * and be consolidated with the fileUriFromPath() method in - * bootstrap.js. - * - * @param {string} path - * @returns {string} - */ - function uriFromPath(path) { - let pathName = path.replace(/\\/g, '/'); - if (pathName.length > 0 && pathName.charAt(0) !== '/') { - pathName = `/${pathName}`; - } - - /** @type {string} */ - let uri; - if (safeProcess.platform === 'win32' && pathName.startsWith('//')) { // specially handle Windows UNC paths - uri = encodeURI(`file:${pathName}`); - } else { - uri = encodeURI(`file://${pathName}`); - } - - return uri.replace(/#/g, '%23'); - } - - /** - * @return {{ mark: (name: string) => void }} - */ - function perfLib() { - globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []; - - return { - /** - * @param {string} name - */ - mark(name) { - globalThis.MonacoPerformanceMarks.push(name, Date.now()); - performance.mark(name); - } - }; - } - return { load, - globals, - perfLib + globals }; })); diff --git a/src/bootstrap.js b/src/bootstrap.js index b447c5320a..ac8ae937a5 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -16,11 +16,7 @@ // Browser else { - try { - globalThis.MonacoBootstrap = factory(); - } catch (error) { - console.warn(error); // expected when e.g. running with sandbox: true (TODO@sandbox eventually consolidate this) - } + globalThis.MonacoBootstrap = factory(); } }(this, function () { const Module = typeof require === 'function' ? require('module') : undefined; @@ -174,94 +170,9 @@ return nlsConfig; } - //#endregion - - - //#region Portable helpers - /** - * @param {{ portable: string | undefined; applicationName: string; }} product - * @returns {{ portableDataPath: string; isPortable: boolean; } | undefined} + * @returns {typeof import('./vs/base/parts/sandbox/electron-sandbox/globals') | undefined} */ - function configurePortable(product) { - if (!path || !fs || typeof process === 'undefined') { - console.warn('configurePortable() is only available in node.js environments'); // TODO@sandbox Portable is currently non-sandboxed only - return; - } - - const appRoot = path.dirname(__dirname); - - /** - * @param {import('path')} path - */ - function getApplicationPath(path) { - if (process.env['VSCODE_DEV']) { - return appRoot; - } - - if (process.platform === 'darwin') { - return path.dirname(path.dirname(path.dirname(appRoot))); - } - - return path.dirname(path.dirname(appRoot)); - } - - /** - * @param {import('path')} path - */ - function getPortableDataPath(path) { - if (process.env['VSCODE_PORTABLE']) { - return process.env['VSCODE_PORTABLE']; - } - - if (process.platform === 'win32' || process.platform === 'linux') { - return path.join(getApplicationPath(path), 'data'); - } - - // @ts-ignore - const portableDataName = product.portable || `${product.applicationName}-portable-data`; - return path.join(path.dirname(getApplicationPath(path)), portableDataName); - } - - const portableDataPath = getPortableDataPath(path); - const isPortable = !('target' in product) && fs.existsSync(portableDataPath); - const portableTempPath = path.join(portableDataPath, 'tmp'); - const isTempPortable = isPortable && fs.existsSync(portableTempPath); - - if (isPortable) { - process.env['VSCODE_PORTABLE'] = portableDataPath; - } else { - delete process.env['VSCODE_PORTABLE']; - } - - if (isTempPortable) { - if (process.platform === 'win32') { - process.env['TMP'] = portableTempPath; - process.env['TEMP'] = portableTempPath; - } else { - process.env['TMPDIR'] = portableTempPath; - } - } - - return { - portableDataPath, - isPortable - }; - } - - //#endregion - - - //#region ApplicationInsights - - // Prevents appinsights from monkey patching modules. - // This should be called before importing the applicationinsights module - function avoidMonkeyPatchFromAppInsights() { - // @ts-ignore - process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] = true; // Skip monkey patching of 3rd party modules by appinsights - global['diagnosticsSource'] = {}; // Prevents diagnostic channel (which patches "require") from initializing entirely - } - function safeGlobals() { const globals = (typeof self === 'object' ? self : typeof global === 'object' ? global : {}); @@ -269,7 +180,7 @@ } /** - * @returns {NodeJS.Process | undefined} + * @returns {import('./vs/base/parts/sandbox/electron-sandbox/globals').IPartialNodeProcess | NodeJS.Process} */ function safeProcess() { if (typeof process !== 'undefined') { @@ -280,16 +191,20 @@ if (globals) { return globals.process; // Native environment (sandboxed) } + + return undefined; } /** - * @returns {Electron.IpcRenderer | undefined} + * @returns {import('./vs/base/parts/sandbox/electron-sandbox/electronTypes').IpcRenderer | undefined} */ function safeIpcRenderer() { const globals = safeGlobals(); if (globals) { return globals.ipcRenderer; } + + return undefined; } /** diff --git a/src/buildfile.js b/src/buildfile.js index 86fadf7901..03f6b41f95 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -10,7 +10,7 @@ function entrypoint(name) { exports.base = [{ name: 'vs/base/common/worker/simpleWorker', include: ['vs/editor/common/services/editorSimpleWorker'], - prepend: ['vs/loader.js'], + prepend: ['vs/loader.js', 'vs/nls.js'], append: ['vs/base/worker/workerMain'], dest: 'vs/base/worker/workerMain.js' }]; diff --git a/src/main.js b/src/main.js index 2f85f31dd3..47b75bba9a 100644 --- a/src/main.js +++ b/src/main.js @@ -7,17 +7,16 @@ 'use strict'; const perf = require('./vs/base/common/performance'); +perf.mark('code/didStartMain'); + const lp = require('./vs/base/node/languagePacks'); - -perf.mark('main:started'); - const path = require('path'); const fs = require('fs'); const os = require('os'); const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); const paths = require('./paths'); -/** @type {any} */ +/** @type {Partial & { applicationName: string}} */ const product = require('../product.json'); const { app, protocol, crashReporter } = require('electron'); @@ -201,14 +200,14 @@ function startup(cachedDataDir, nlsConfig) { process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; // Load main in AMD - perf.mark('willLoadMainBundle'); + perf.mark('code/willLoadMainBundle'); require('./bootstrap-amd').load('vs/code/electron-main/main', () => { - perf.mark('didLoadMainBundle'); + perf.mark('code/didLoadMainBundle'); }); } async function onReady() { - perf.mark('main:appReady'); + perf.mark('code/mainAppReady'); try { const [cachedDataDir, nlsConfig] = await Promise.all([nodeCachedDataDir.ensureExists(), resolveNlsConfiguration()]); @@ -220,9 +219,7 @@ async function onReady() { } /** - * @typedef {{ [arg: string]: any; '--'?: string[]; _: string[]; }} NativeParsedArgs - * - * @param {NativeParsedArgs} cliArgs + * @param {import('./vs/platform/environment/common/argv').NativeParsedArgs} cliArgs */ function configureCommandlineSwitchesSync(cliArgs) { const SUPPORTED_ELECTRON_SWITCHES = [ @@ -246,7 +243,11 @@ function configureCommandlineSwitchesSync(cliArgs) { const SUPPORTED_MAIN_PROCESS_SWITCHES = [ // Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775 - 'enable-proposed-api' + 'enable-proposed-api', + + // TODO@bpasero remove me once testing is done on `vscode-file` protocol + // (all traces of `enable-browser-code-loading` and `ENABLE_VSCODE_BROWSER_CODE_LOADING`) + 'enable-browser-code-loading' ]; // Read argv config @@ -277,12 +278,20 @@ function configureCommandlineSwitchesSync(cliArgs) { // Append main process flags to process.argv else if (SUPPORTED_MAIN_PROCESS_SWITCHES.indexOf(argvKey) !== -1) { - if (argvKey === 'enable-proposed-api') { - if (Array.isArray(argvValue)) { - argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id)); - } else { - console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`); - } + switch (argvKey) { + case 'enable-proposed-api': + if (Array.isArray(argvValue)) { + argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id)); + } else { + console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`); + } + break; + + case 'enable-browser-code-loading': + if (typeof argvValue === 'string') { + process.env['ENABLE_VSCODE_BROWSER_CODE_LOADING'] = argvValue; + } + break; } } }); @@ -293,6 +302,11 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch('js-flags', jsFlags); } + // Support __sandbox flag + if (cliArgs.__sandbox) { + process.env['ENABLE_VSCODE_BROWSER_CODE_LOADING'] = 'bypassHeatCheck'; + } + return argvConfig; } @@ -375,7 +389,7 @@ function getArgvConfigPath() { } /** - * @param {NativeParsedArgs} cliArgs + * @param {import('./vs/platform/environment/common/argv').NativeParsedArgs} cliArgs * @returns {string | null} */ function getJSFlags(cliArgs) { @@ -395,7 +409,7 @@ function getJSFlags(cliArgs) { } /** - * @param {NativeParsedArgs} cliArgs + * @param {import('./vs/platform/environment/common/argv').NativeParsedArgs} cliArgs * * @returns {string} */ @@ -408,7 +422,7 @@ function getUserDataPath(cliArgs) { } /** - * @returns {NativeParsedArgs} + * @returns {import('./vs/platform/environment/common/argv').NativeParsedArgs} */ function parseCLIArgs() { const minimist = require('minimist'); @@ -457,11 +471,16 @@ function registerListeners() { * @type {string[]} */ const openUrls = []; - const onOpenUrl = function (event, url) { - event.preventDefault(); + const onOpenUrl = + /** + * @param {{ preventDefault: () => void; }} event + * @param {string} url + */ + function (event, url) { + event.preventDefault(); - openUrls.push(url); - }; + openUrls.push(url); + }; app.on('will-finish-launching', function () { app.on('open-url', onOpenUrl); diff --git a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts index 8ff430d988..6d9652cbb6 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -159,7 +159,7 @@ export class HeaderFilter { private createButtonMenuItem(title: string, command: HeaderFilterCommands, iconClass: string): Button { const buttonContainer = append(this.menu, $('')); const button = new Button(buttonContainer); - button.icon = { classNames: `slick-header-menuicon ${iconClass}` }; + button.icon = { id: `slick-header-menuicon ${iconClass}` }; button.label = title; button.onDidClick(async () => { await this.handleMenuItemClick(command, this.columnDef); diff --git a/src/sql/base/test/browser/ui/taskbar/overflowActionbar.test.ts b/src/sql/base/test/browser/ui/taskbar/overflowActionbar.test.ts index a7bbb14a6b..84fabd9960 100644 --- a/src/sql/base/test/browser/ui/taskbar/overflowActionbar.test.ts +++ b/src/sql/base/test/browser/ui/taskbar/overflowActionbar.test.ts @@ -17,62 +17,59 @@ suite('Overflow Actionbar tests', () => { }); test('Verify moving actions from toolbar to overflow', () => { - assert(overflowActionbar.actionsList.children.length === 0); - assert(overflowActionbar.items.length === 0); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 0); + assert.strictEqual(overflowActionbar.items.length, 0); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); let a1 = new Action('a1'); let a2 = new Action('a1'); let a3 = new Action('a3'); overflowActionbar.pushAction([a1, a2, a3]); - assert(overflowActionbar.actionsList.children.length === 3); - assert(overflowActionbar.items.length === 3); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); // more item element '...' is added when actions are moved to the overflow // and a placeholder undefined element is added to the items array for calculating focus overflowActionbar.createMoreItemElement(); - assert(overflowActionbar.actionsList.children.length === 4); - assert(overflowActionbar.items.length === 4); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 4); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); // move a3 to overflow overflowActionbar.collapseItem(); - assert(overflowActionbar.actionsList.children.length === 3); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 2); - assert(overflowActionbar.overflow.childElementCount === 1); + assert.strictEqual(overflowActionbar.actionsList.children.length, 3); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 2); verifyOverflowFocusedIndex(overflowActionbar, 3); // move a2 to overflow overflowActionbar.collapseItem(); - assert(overflowActionbar.actionsList.children.length === 2); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 1); - assert(overflowActionbar.overflow.childElementCount === 2); + assert.strictEqual(overflowActionbar.actionsList.children.length, 2); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 1); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 2); verifyOverflowFocusedIndex(overflowActionbar, 2); // move a2 to back to toolbar overflowActionbar.restoreItem(); - assert(overflowActionbar.actionsList.children.length === 3); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 2); - assert(overflowActionbar.overflow.childElementCount === 1); + assert.strictEqual(overflowActionbar.actionsList.children.length, 3); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 2); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 1); verifyOverflowFocusedIndex(overflowActionbar, 3); // move a3 to back to toolbar overflowActionbar.restoreItem(); - assert(overflowActionbar.actionsList.children.length === 4); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 3); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 4); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 3); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); }); test('Verify moving actions and separators from toolbar to overflow', () => { - assert(overflowActionbar.actionsList.children.length === 0); - assert(overflowActionbar.items.length === 0); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 0); + assert.strictEqual(overflowActionbar.items.length, 0); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); let a1 = new Action('a1'); let a2 = new Action('a1'); @@ -82,70 +79,70 @@ suite('Overflow Actionbar tests', () => { overflowActionbar.pushElement(separator); overflowActionbar.pushAction([a3]); - assert(overflowActionbar.actionsList.children.length === 4); - assert(overflowActionbar.items.length === 3); // items only has focusable elements - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 4); + assert.strictEqual(overflowActionbar.items.length, 3); // items only has focusable elements + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); // more item element '...' is added when actions are moved to the overflow // and a placeholder undefined element is added to the items array for calculating focus overflowActionbar.createMoreItemElement(); - assert(overflowActionbar.actionsList.children.length === 5); - assert(overflowActionbar.items.length === 4); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 5); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); // move a3 to overflow overflowActionbar.collapseItem(); - assert(overflowActionbar.actionsList.children.length === 4); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 2); - assert(overflowActionbar.overflow.childElementCount === 1); + assert.strictEqual(overflowActionbar.actionsList.children.length, 4); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 2); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 1); verifyOverflowFocusedIndex(overflowActionbar, 3); // move separator to overflow overflowActionbar.collapseItem(); - assert(overflowActionbar.actionsList.children.length === 3); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 2); - assert(overflowActionbar.overflow.childElementCount === 2); + assert.strictEqual(overflowActionbar.actionsList.children.length, 3); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 2); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 2); verifyOverflowFocusedIndex(overflowActionbar, 3); // move a2 to overflow overflowActionbar.collapseItem(); - assert(overflowActionbar.actionsList.children.length === 2); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 1); - assert(overflowActionbar.overflow.childElementCount === 3); + assert.strictEqual(overflowActionbar.actionsList.children.length, 2); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 1); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 3); verifyOverflowFocusedIndex(overflowActionbar, 2); // move a2 to back to toolbar overflowActionbar.restoreItem(); - assert(overflowActionbar.actionsList.children.length === 3); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 2); - assert(overflowActionbar.overflow.childElementCount === 2); + assert.strictEqual(overflowActionbar.actionsList.children.length, 3); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 2); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 2); verifyOverflowFocusedIndex(overflowActionbar, 3); // move separator to back to toolbar overflowActionbar.restoreItem(); - assert(overflowActionbar.actionsList.children.length === 4); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 2); - assert(overflowActionbar.overflow.childElementCount === 1); + assert.strictEqual(overflowActionbar.actionsList.children.length, 4); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 2); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 1); verifyOverflowFocusedIndex(overflowActionbar, 3); // move a3 to back to toolbar overflowActionbar.restoreItem(); - assert(overflowActionbar.actionsList.children.length === 5); - assert(overflowActionbar.items.length === 4); - assert(getMoreItemPlaceholderIndex(overflowActionbar.items) === 3); - assert(overflowActionbar.overflow.childElementCount === 0); + assert.strictEqual(overflowActionbar.actionsList.children.length, 5); + assert.strictEqual(overflowActionbar.items.length, 4); + assert.strictEqual(getMoreItemPlaceholderIndex(overflowActionbar.items), 3); + assert.strictEqual(overflowActionbar.overflow.childElementCount, 0); }); }); function verifyOverflowFocusedIndex(overflowToolbar: OverflowActionBar, expectedIndex: number) { // click more item element to show overflow and set focus to first element overflowToolbar.moreElementOnClick(new MouseEvent('click')); - assert(overflowToolbar.focusedItem === expectedIndex); + assert.strictEqual(overflowToolbar.focusedItem, expectedIndex); // click to hide overflow overflowToolbar.moreElementOnClick(new MouseEvent('click')); } diff --git a/src/sql/base/test/parts/editableDropdown/browser/dropdown.test.ts b/src/sql/base/test/parts/editableDropdown/browser/dropdown.test.ts index a454bbcca6..ab6436309a 100644 --- a/src/sql/base/test/parts/editableDropdown/browser/dropdown.test.ts +++ b/src/sql/base/test/parts/editableDropdown/browser/dropdown.test.ts @@ -26,7 +26,7 @@ suite('Editable dropdown tests', () => { test('default value for editable dropdown is empty', () => { const dropdown = new Dropdown(container, undefined, options); - assert(dropdown.value === ''); + assert.strictEqual(dropdown.value, ''); }); test('changing value through code fires onValueChange event', () => { @@ -37,11 +37,11 @@ suite('Editable dropdown tests', () => { }); dropdown.value = options.values[0]; - assert(count === 1, 'onValueChange event was not fired'); + assert.strictEqual(count, 1, 'onValueChange event was not fired'); dropdown.value = options.values[0]; - assert(count === 1, 'onValueChange event should not be fired for setting the same value again'); + assert.strictEqual(count, 1, 'onValueChange event should not be fired for setting the same value again'); dropdown.value = options.values[1]; - assert(count === 2, 'onValueChange event was not fired for setting a new value of the dropdown'); + assert.strictEqual(count, 2, 'onValueChange event was not fired for setting a new value of the dropdown'); }); test('changing value through input text fires onValue Change event', () => { @@ -54,18 +54,18 @@ suite('Editable dropdown tests', () => { dropdown.fireOnTextChange = true; dropdown.setDropdownVisibility(true); dropdown.input.value = options.values[0]; - assert(count === 1, 'onValueChange event was not fired for an option from the dropdown list'); + assert.strictEqual(count, 1, 'onValueChange event was not fired for an option from the dropdown list'); dropdown.input.value = 'foo'; - assert(count === 2, 'onValueChange event was not fired for a value not in dropdown list'); - assert(dropdown.selectList.length === 4, 'list does not have all the values that are matching the input box text'); - assert(dropdown.value = 'foo'); + assert.strictEqual(count, 2, 'onValueChange event was not fired for a value not in dropdown list'); + assert.strictEqual(dropdown.selectList.length, 4, 'list does not have all the values that are matching the input box text'); + assert.strictEqual(dropdown.value, 'foo'); dropdown.input.value = 'foobar'; - assert(count === 3, 'onValueChange event was not fired for a value not in dropdown list'); - assert(dropdown.selectList.length === 2, 'list does not have all the values that are matching the input box text'); - assert(dropdown.value = 'foobar'); + assert.strictEqual(count, 3, 'onValueChange event was not fired for a value not in dropdown list'); + assert.strictEqual(dropdown.selectList.length, 2, 'list does not have all the values that are matching the input box text'); + assert.strictEqual(dropdown.value, 'foobar'); dropdown.fireOnTextChange = false; dropdown.input.value = options.values[0]; - assert(count === 3, 'onValueChange event was fired with input box value change even after setting the fireOnTextChange to false'); + assert.strictEqual(count, 3, 'onValueChange event was fired with input box value change even after setting the fireOnTextChange to false'); }); }); diff --git a/src/sql/workbench/browser/actions/layoutActions.ts b/src/sql/workbench/browser/actions/layoutActions.ts new file mode 100644 index 0000000000..e0a2d5fdc4 --- /dev/null +++ b/src/sql/workbench/browser/actions/layoutActions.ts @@ -0,0 +1,35 @@ +import { Action } from 'vs/base/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { FocusedViewContext, IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; + +// --- Toggle View with Command +export abstract class ToggleViewAction extends Action { + + constructor( + id: string, + label: string, + private readonly viewId: string, + protected viewsService: IViewsService, + protected viewDescriptorService: IViewDescriptorService, + protected contextKeyService: IContextKeyService, + private layoutService: IWorkbenchLayoutService, + cssClass?: string + ) { + super(id, label, cssClass); + } + + async run(): Promise { + const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + + if (focusedViewId === this.viewId) { + if (this.viewDescriptorService.getViewLocationById(this.viewId) === ViewContainerLocation.Sidebar) { + this.layoutService.setSideBarHidden(true); + } else { + this.layoutService.setPanelHidden(true); + } + } else { + this.viewsService.openView(this.viewId, true); + } + } +} diff --git a/src/sql/workbench/browser/modal/modal.ts b/src/sql/workbench/browser/modal/modal.ts index 09a7489e08..2988fdd6a9 100644 --- a/src/sql/workbench/browser/modal/modal.ts +++ b/src/sql/workbench/browser/modal/modal.ts @@ -240,7 +240,7 @@ export abstract class Modal extends Disposable implements IThemable { const container = DOM.append(this._modalHeaderSection, DOM.$('.modal-go-back')); this._backButton = new Button(container, { secondary: true }); this._backButton.icon = { - classNames: 'backButtonIcon' + id: 'backButtonIcon' }; this._backButton.title = localize('modal.back', "Back"); } @@ -262,21 +262,21 @@ export abstract class Modal extends Disposable implements IThemable { this._detailsButtonContainer = DOM.append(headerContainer, DOM.$('.dialog-message-button')); this._toggleMessageDetailButton = new Button(this._detailsButtonContainer); this._toggleMessageDetailButton.icon = { - classNames: 'message-details-icon' + id: 'message-details-icon' }; this._toggleMessageDetailButton.label = SHOW_DETAILS_TEXT; this._register(this._toggleMessageDetailButton.onDidClick(() => this.toggleMessageDetail())); const copyMessageButtonContainer = DOM.append(headerContainer, DOM.$('.dialog-message-button')); this._copyMessageButton = new Button(copyMessageButtonContainer); this._copyMessageButton.icon = { - classNames: 'copy-message-icon' + id: 'copy-message-icon' }; this._copyMessageButton.label = COPY_TEXT; this._register(this._copyMessageButton.onDidClick(() => this._clipboardService.writeText(this.getTextForClipboard()))); const closeMessageButtonContainer = DOM.append(headerContainer, DOM.$('.dialog-message-button')); this._closeMessageButton = new Button(closeMessageButtonContainer); this._closeMessageButton.icon = { - classNames: 'close-message-icon' + id: 'close-message-icon' }; this._closeMessageButton.label = CLOSE_TEXT; this._register(this._closeMessageButton.onDidClick(() => this.setError(undefined))); diff --git a/src/sql/workbench/browser/modelComponents/button.component.ts b/src/sql/workbench/browser/modelComponents/button.component.ts index f8962ebeeb..1a97ee21a5 100644 --- a/src/sql/workbench/browser/modelComponents/button.component.ts +++ b/src/sql/workbench/browser/modelComponents/button.component.ts @@ -177,7 +177,7 @@ export default class ButtonComponent extends ComponentWithIconBase { - return this._proxy.$resolve(this.treeViewId, element.handle); + return this._proxy.$resolve(this.treeViewId, element.handle, undefined); } : undefined); this.itemsMap.set(element.handle, resolvable); result.push(resolvable); diff --git a/src/sql/workbench/contrib/assessment/test/browser/asmtActions.test.ts b/src/sql/workbench/contrib/assessment/test/browser/asmtActions.test.ts index 5ce1de1b76..5de8828f3f 100644 --- a/src/sql/workbench/contrib/assessment/test/browser/asmtActions.test.ts +++ b/src/sql/workbench/contrib/assessment/test/browser/asmtActions.test.ts @@ -24,7 +24,7 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; import { NullLogService } from 'vs/platform/log/common/log'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; -import { OpenerServiceStub } from 'sql/platform/opener/common/openerServiceStub'; +import { OpenerServiceStub } from 'sql/workbench/contrib/opener/common/openerServiceStub'; import { SqlAssessmentTargetType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestFileService, TestEnvironmentService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts index 0e6946fba7..b1e6c5445e 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts @@ -18,7 +18,7 @@ import { } from 'sql/workbench/services/objectExplorer/browser/connectionTreeAction'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts index ce7c75a9c4..d0f549f55f 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts @@ -6,27 +6,14 @@ import 'vs/css!./media/dataExplorer.contribution'; import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { DataExplorerViewletViewsContribution, OpenDataExplorerViewletAction } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { DataExplorerContainerExtensionHandler } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerExtensionPoint'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { DataExplorerViewletViewsContribution } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet'; const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(DataExplorerViewletViewsContribution, LifecyclePhase.Starting); -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction( - SyncActionDescriptor.create( - OpenDataExplorerViewletAction, - OpenDataExplorerViewletAction.ID, - OpenDataExplorerViewletAction.LABEL, - { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D }), - 'View: Show Data Explorer', - localize('dataExplorer.view', "View") -); let configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts index 7036d5ff39..e999620a60 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts @@ -22,30 +22,14 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { Registry } from 'vs/platform/registry/common/platform'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { ViewPaneContainer, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Viewlet } from 'vs/workbench/browser/viewlet'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; export const VIEWLET_ID = 'workbench.view.connections'; -// Viewlet Action -export class OpenDataExplorerViewletAction extends ShowViewletAction { - public static ID = VIEWLET_ID; - public static LABEL = localize('showDataExplorer', "Show Connections"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); - } -} - export class DataExplorerViewletViewsContribution implements IWorkbenchContribution { constructor() { @@ -152,9 +136,15 @@ export const dataExplorerIconId = 'dataExplorer'; export const VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, - name: localize('dataexplorer.name', "Connections"), + title: localize('dataexplorer.name', "Connections"), ctorDescriptor: new SyncDescriptor(DataExplorerViewPaneContainer), + openCommandActionDescriptor: { + id: VIEWLET_ID, + mnemonicTitle: localize('showDataExplorer', "Show Connections"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D }, + order: 0 + }, icon: { id: 'dataExplorer' }, order: 0, storageId: `${VIEWLET_ID}.state` -}, ViewContainerLocation.Sidebar, true); +}, ViewContainerLocation.Sidebar, { isDefault: true }); diff --git a/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts b/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts index 4527f6dd48..8c2067ed92 100644 --- a/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts +++ b/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts @@ -71,7 +71,8 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel this._decorations = Object.create(null); - this._buffer = createTextBuffer('', NotebookFindModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + const { textBuffer, } = createTextBuffer('', NotebookFindModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + this._buffer = textBuffer; this._versionId = 1; this.id = '$model' + MODEL_ID; } @@ -287,7 +288,8 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel */ private _validateRangeRelaxedNoAllocations(range: IRange): NotebookRange { if (range instanceof NotebookRange) { - this._buffer = createTextBuffer(range.cell.source instanceof Array ? range.cell.source.join('\n') : range.cell.source, NotebookFindModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + const { textBuffer, } = createTextBuffer(range.cell.source instanceof Array ? range.cell.source.join('\n') : range.cell.source, NotebookFindModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + this._buffer = textBuffer; } const linesCount = this._buffer.getLineCount(); diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index ec31e8fea8..177a72db1b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -260,7 +260,13 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let errorWithAction = createErrorWithActions(toErrorMessage(error), { actions: [ new Action('workbench.files.action.createMissingFile', localize('createFile', "Create File"), undefined, true, () => { - return this.textFileService.create(this.notebookParams.notebookUri).then(() => this.editorService.openEditor({ + let operations = new Array(1); + operations[0] = { + resource: this.notebookParams.notebookUri, + value: undefined, + options: undefined + }; + return this.textFileService.create(operations).then(() => this.editorService.openEditor({ resource: this.notebookParams.notebookUri, options: { pinned: true // new file gets pinned by default diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index 8edb280923..7ac71acf2b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -46,7 +46,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { NotebookThemingContribution } from 'sql/workbench/contrib/notebook/browser/notebookThemingContribution'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; -import { NotebookExplorerViewletViewsContribution, OpenNotebookExplorerViewletAction } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet'; +import { NotebookExplorerViewletViewsContribution } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet'; import 'vs/css!./media/notebook.contribution'; import { isMacintosh } from 'vs/base/common/platform'; import { SearchSortOrder } from 'vs/workbench/services/search/common/search'; @@ -439,16 +439,6 @@ registerCellComponent(TextCellComponent); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(NotebookExplorerViewletViewsContribution, LifecyclePhase.Starting); -const registry = Registry.as(WorkbenchActionsExtensions.WorkbenchActions); -registry.registerWorkbenchAction( - SyncActionDescriptor.create( - OpenNotebookExplorerViewletAction, - OpenNotebookExplorerViewletAction.ID, - OpenNotebookExplorerViewletAction.LABEL, - { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B }), - 'View: Show Notebook Explorer', - localize('notebookExplorer.view', "View") -); // Configuration configurationRegistry.registerConfiguration({ diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts index d3a304c09f..d73623011d 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet.ts @@ -21,10 +21,9 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { Registry } from 'vs/platform/registry/common/platform'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { ViewPaneContainer, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { Viewlet } from 'vs/workbench/browser/viewlet'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { NotebookSearchWidget, INotebookExplorerSearchOptions } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearchWidget'; import * as Constants from 'sql/workbench/common/constants'; @@ -44,22 +43,6 @@ import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; export const VIEWLET_ID = 'workbench.view.notebooks'; -// Viewlet Action -export class OpenNotebookExplorerViewletAction extends ShowViewletAction { - public static ID = VIEWLET_ID; - public static LABEL = localize('showNotebookExplorer', "Show Notebooks"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); - } -} - export class NotebookExplorerViewletViewsContribution implements IWorkbenchContribution { constructor() { @@ -450,7 +433,7 @@ export const notebookIconId = 'book'; export const NOTEBOOK_VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, - name: localize('notebookExplorer.name', "Notebooks"), + title: localize('notebookExplorer.name', "Notebooks"), ctorDescriptor: new SyncDescriptor(NotebookExplorerViewPaneContainer), icon: { id: notebookIconId }, order: 6, diff --git a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts index ba9884548e..e6dbff3e49 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookExplorer/notebookSearch.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SearchView, SearchUIState } from 'vs/workbench/contrib/search/browser/searchView'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IFileService } from 'vs/platform/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -43,6 +43,7 @@ import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchStop import { Action, IAction } from 'vs/base/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Memento } from 'vs/workbench/common/memento'; +import { SearchUIState } from 'vs/workbench/contrib/search/common/search'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; @@ -119,10 +120,6 @@ export class NotebookSearchView extends SearchView { } public updateActions(): void { - if (!this.isVisible()) { - this.updatedActionsWhileHidden = true; - } - for (const action of this.viewActions) { action.update(); } diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts index 901a3ffc84..ab9cf7c62e 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts @@ -6,6 +6,9 @@ import { nb } from 'azdata'; import * as assert from 'assert'; +import * as fs from 'fs'; +import * as pfs from 'vs/base/node/pfs'; + import { URI } from 'vs/base/common/uri'; import * as tempWrite from 'temp-write'; import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager'; @@ -13,9 +16,9 @@ import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IFileService, IReadFileOptions, IFileContent, IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; -import * as pfs from 'vs/base/node/pfs'; import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { isUndefinedOrNull } from 'vs/base/common/types'; +import { promisify } from 'util'; let expectedNotebookContent: nb.INotebookContents = { cells: [{ @@ -53,7 +56,8 @@ suite('Local Content Manager', function (): void { const instantiationService = new TestInstantiationService(); const fileService = new class extends TestFileService { async readFile(resource: URI, options?: IReadFileOptions | undefined): Promise { - const content = await pfs.readFile(resource.fsPath); + const content = await promisify(fs.readFile)(resource.fsPath); + return { name: ',', size: 0, etag: '', mtime: 0, value: VSBuffer.fromString(content.toString()), resource, ctime: 0 }; } async writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise { diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts index 41c690f810..d216af868b 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookEditorModel.test.ts @@ -977,7 +977,7 @@ suite('Notebook Editor Model', function (): void { await notebookModel.loadContents(); } - async function createTextEditorModel(self: Mocha.ITestCallbackContext): Promise { + async function createTextEditorModel(self: Mocha.Context): Promise { let textFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(self, defaultUri.toString()), 'utf8', undefined); (accessor.textFileService.files).add(textFileEditorModel.resource, textFileEditorModel); await textFileEditorModel.load(); diff --git a/src/sql/platform/opener/common/openerServiceStub.ts b/src/sql/workbench/contrib/opener/common/openerServiceStub.ts similarity index 75% rename from src/sql/platform/opener/common/openerServiceStub.ts rename to src/sql/workbench/contrib/opener/common/openerServiceStub.ts index 2c0dda3bdb..062ef34763 100644 --- a/src/sql/platform/opener/common/openerServiceStub.ts +++ b/src/sql/workbench/contrib/opener/common/openerServiceStub.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; import { IOpenerService, IOpener, IValidator, IExternalUriResolver, IExternalOpener, ResolveExternalUriOptions, IResolvedExternalUri } from 'vs/platform/opener/common/opener'; +import { IExternalOpenerProvider } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -25,4 +26,13 @@ export class OpenerServiceStub implements IOpenerService { } _serviceBrand: undefined; async open(resource: URI | string, options?: any): Promise { return Promise.resolve(true); } + setDefaultExternalOpener(opener: IExternalOpener): void { + throw new Error('Method not implemented.'); + } + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable { + throw new Error('Method not implemented.'); + } + registerExternalOpener(opener: IExternalOpener): IDisposable { + throw new Error('Method not implemented.'); + } } diff --git a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts index b2b98dc772..7d81165638 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorOptions, InDiffEditorState } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; @@ -60,7 +60,7 @@ export class ProfilerResourceEditor extends BaseTextEditor { const options = super.getConfigurationOverrides(); options.readOnly = true; if (this.input) { - options.inDiffEditor = InDiffEditorState.SideBySideLeft; + options.inDiffEditor = false; options.scrollBeyondLastLine = false; options.folding = false; options.renderWhitespace = 'none'; diff --git a/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts b/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts index d1bd1edc91..0d0139b81e 100644 --- a/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts @@ -9,7 +9,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import * as DOM from 'vs/base/browser/dom'; import * as types from 'vs/base/common/types'; @@ -34,8 +34,7 @@ export class BareResultsGridInfo extends BareFontInfo { cellPadding?: number | number[]; }, zoomLevel: number): BareResultsGridInfo { let cellPadding = !types.isUndefinedOrNull(opts.cellPadding) ? opts.cellPadding : RESULTS_GRID_DEFAULTS.cellPadding; - - return new BareResultsGridInfo(BareFontInfo.createFromRawSettings(opts, zoomLevel), { cellPadding }); + return new BareResultsGridInfo(BareFontInfo.createFromRawSettings(opts, zoomLevel, getPixelRatio()), { cellPadding }); } readonly cellPadding: number | number[]; diff --git a/src/sql/workbench/contrib/queryHistory/browser/queryHistoryActions.ts b/src/sql/workbench/contrib/queryHistory/browser/queryHistoryActions.ts index 3bb7a0f8d7..ec1e07cb5a 100644 --- a/src/sql/workbench/contrib/queryHistory/browser/queryHistoryActions.ts +++ b/src/sql/workbench/contrib/queryHistory/browser/queryHistoryActions.ts @@ -16,7 +16,7 @@ import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; -import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ToggleViewAction } from 'sql/workbench/browser/actions/layoutActions'; export class ToggleQueryHistoryAction extends ToggleViewAction { diff --git a/src/sql/workbench/contrib/queryHistory/browser/queryHistoryView.ts b/src/sql/workbench/contrib/queryHistory/browser/queryHistoryView.ts index 4eefc9740f..a7adddbfc3 100644 --- a/src/sql/workbench/contrib/queryHistory/browser/queryHistoryView.ts +++ b/src/sql/workbench/contrib/queryHistory/browser/queryHistoryView.ts @@ -21,7 +21,7 @@ import { IQueryHistoryService } from 'sql/workbench/services/queryHistory/common import { QueryHistoryNode } from 'sql/workbench/contrib/queryHistory/browser/queryHistoryNode'; import { QueryHistoryInfo } from 'sql/workbench/services/queryHistory/common/queryHistoryInfo'; import { IAction } from 'vs/base/common/actions'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; diff --git a/src/sql/workbench/contrib/queryHistory/electron-browser/queryHistory.ts b/src/sql/workbench/contrib/queryHistory/electron-browser/queryHistory.ts index c8a5a0a48a..6d963ba16e 100644 --- a/src/sql/workbench/contrib/queryHistory/electron-browser/queryHistory.ts +++ b/src/sql/workbench/contrib/queryHistory/electron-browser/queryHistory.ts @@ -96,14 +96,11 @@ export class QueryHistoryWorkbenchContribution implements IWorkbenchContribution // markers view container const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: QUERY_HISTORY_CONTAINER_ID, - name: localize('queryHistory', "Query History"), + title: localize('queryHistory', "Query History"), hideIfEmpty: true, order: 20, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [QUERY_HISTORY_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: `${QUERY_HISTORY_CONTAINER_ID}.storage`, - focusCommand: { - id: ToggleQueryHistoryAction.ID - } }, ViewContainerLocation.Panel); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts index 40dc9570cd..f2ad6069e6 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewer.contribution.ts @@ -68,7 +68,7 @@ function registerResourceViewerContainer() { const resourceViewerIcon = registerCodicon('reosurce-view', Codicon.database); const viewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: RESOURCE_VIEWER_VIEW_CONTAINER_ID, - name: localize('resourceViewer', "Resource Viewer"), + title: localize('resourceViewer', "Resource Viewer"), ctorDescriptor: new SyncDescriptor(ResourceViewerViewlet), icon: resourceViewerIcon, alwaysUseContainerInfo: true diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts index 60b2566c30..f89e10454a 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerView.ts @@ -20,7 +20,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; diff --git a/src/sql/workbench/contrib/tasks/browser/tasks.contribution.ts b/src/sql/workbench/contrib/tasks/browser/tasks.contribution.ts index fb44e311c8..9da559a377 100644 --- a/src/sql/workbench/contrib/tasks/browser/tasks.contribution.ts +++ b/src/sql/workbench/contrib/tasks/browser/tasks.contribution.ts @@ -75,14 +75,11 @@ registry.registerWorkbenchAction( // markers view container const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: TASKS_CONTAINER_ID, - name: localize('tasks', "Tasks"), + title: localize('tasks', "Tasks"), hideIfEmpty: true, order: 20, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TASKS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), - storageId: `${TASKS_CONTAINER_ID}.storage`, - focusCommand: { - id: ToggleTasksAction.ID - } + storageId: `${TASKS_CONTAINER_ID}.storage` }, ViewContainerLocation.Panel); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ diff --git a/src/sql/workbench/contrib/tasks/browser/tasksActions.ts b/src/sql/workbench/contrib/tasks/browser/tasksActions.ts index cbeea8e5bf..ba69155216 100644 --- a/src/sql/workbench/contrib/tasks/browser/tasksActions.ts +++ b/src/sql/workbench/contrib/tasks/browser/tasksActions.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ToggleViewAction } from 'sql/workbench/browser/actions/layoutActions'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TASKS_VIEW_ID } from 'sql/workbench/contrib/tasks/common/tasks'; diff --git a/src/sql/workbench/contrib/tasks/browser/tasksView.ts b/src/sql/workbench/contrib/tasks/browser/tasksView.ts index a478bd6d06..ac2ce2e1b5 100644 --- a/src/sql/workbench/contrib/tasks/browser/tasksView.ts +++ b/src/sql/workbench/contrib/tasks/browser/tasksView.ts @@ -14,7 +14,7 @@ import { ITree } from 'vs/base/parts/tree/browser/tree'; import { DefaultFilter, DefaultDragAndDrop, DefaultAccessibilityProvider } from 'vs/base/parts/tree/browser/treeDefaults'; import { localize } from 'vs/nls'; import { hide, $, append } from 'vs/base/browser/dom'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; diff --git a/src/sql/workbench/services/accountManagement/browser/accountDialog.ts b/src/sql/workbench/services/accountManagement/browser/accountDialog.ts index 0726f8328c..53ca70eacf 100644 --- a/src/sql/workbench/services/accountManagement/browser/accountDialog.ts +++ b/src/sql/workbench/services/accountManagement/browser/accountDialog.ts @@ -32,7 +32,8 @@ import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { ILogService } from 'vs/platform/log/common/log'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; -import { IViewPaneOptions, ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { attachModalDialogStyler, attachPanelStyler } from 'sql/workbench/common/styler'; import { IViewDescriptorService, IViewsRegistry, Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -53,7 +54,7 @@ export class AccountPaneContainer extends ViewPaneContainer { export const ACCOUNT_VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, - name: localize('accountExplorer.name', "Accounts"), + title: localize('accountExplorer.name', "Accounts"), ctorDescriptor: new SyncDescriptor(AccountPaneContainer), storageId: `${VIEWLET_ID}.state` }, ViewContainerLocation.Dialog); diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts index 7503482e17..46e13f0158 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts @@ -70,7 +70,7 @@ suite('ConnectionDialogService tests', () => { setup(() => { const viewInstantiationService: TestInstantiationService = workbenchInstantiationService(); const viewDescriptorService = viewInstantiationService.createInstance(ViewDescriptorService); - container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); viewInstantiationService.stub(IViewDescriptorService, viewDescriptorService); const viewDescriptor: ITreeViewDescriptor = { id: testTreeViewId, diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts index e98e2d9a38..e4ff3bbc43 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts @@ -42,7 +42,7 @@ suite('ConnectionDialogWidget tests', () => { setup(() => { const viewInstantiationService: TestInstantiationService = workbenchInstantiationService(); const viewDescriptorService = viewInstantiationService.createInstance(ViewDescriptorService); - container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); viewInstantiationService.stub(IViewDescriptorService, viewDescriptorService); const viewDescriptor: ITreeViewDescriptor = { id: testTreeViewId, diff --git a/src/sql/workbench/services/connection/test/browser/testTreeView.ts b/src/sql/workbench/services/connection/test/browser/testTreeView.ts index 599c1c74d7..7ebcd19814 100644 --- a/src/sql/workbench/services/connection/test/browser/testTreeView.ts +++ b/src/sql/workbench/services/connection/test/browser/testTreeView.ts @@ -842,7 +842,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer { if (node instanceof ResolvableTreeItem) { - await node.resolve(); + await node.resolve(undefined); } let tooltip: IMarkdownString | string | undefined = node.tooltip ?? label; if (isHovering && tooltip) { diff --git a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts index 325b6eb5b5..78da2f953b 100644 --- a/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts +++ b/src/sql/workbench/services/errorMessage/browser/errorMessageDialog.ts @@ -81,7 +81,7 @@ export class ErrorMessageDialog extends Modal { } }, 'left', true); this._copyButton!.icon = { - classNames: 'codicon scriptToClipboard' + id: 'codicon scriptToClipboard' }; this._copyButton!.element.title = copyButtonLabel; this._register(attachButtonStyler(this._copyButton!, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND, buttonForeground: SIDE_BAR_FOREGROUND })); diff --git a/src/sql/workbench/services/insights/browser/insightsDialogView.ts b/src/sql/workbench/services/insights/browser/insightsDialogView.ts index c037b8bfe3..66c52dd30f 100644 --- a/src/sql/workbench/services/insights/browser/insightsDialogView.ts +++ b/src/sql/workbench/services/insights/browser/insightsDialogView.ts @@ -39,7 +39,8 @@ import { TaskRegistry } from 'sql/workbench/services/tasks/browser/tasksRegistry import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { ViewPane, IViewPaneOptions, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { attachPanelStyler, attachModalDialogStyler } from 'sql/workbench/common/styler'; import { IViewDescriptorService, IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -61,7 +62,7 @@ export class InsightsDetailPaneContainer extends ViewPaneContainer { } export const INSIGHTS_DETAIL_VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, - name: nls.localize('insightsDetailView.name', "Insight Details"), + title: nls.localize('insightsDetailView.name', "Insight Details"), ctorDescriptor: new SyncDescriptor(InsightsDetailPaneContainer), storageId: `${VIEWLET_ID}.state` }, ViewContainerLocation.Dialog); diff --git a/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts b/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts index dbd79be392..f8294c9df0 100644 --- a/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts +++ b/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts @@ -32,7 +32,7 @@ const testColumns: string[] = [ ]; suite('Insights Dialog Controller Tests', () => { - test('updates correctly with good input', async (done) => { + test('updates correctly with good input', (done) => { let model = new InsightsDialogModel(); @@ -82,19 +82,21 @@ suite('Insights Dialog Controller Tests', () => { options: {} }; - await controller.update({ query: 'query' }, profile); - // Once we update the controller, listen on when it changes the model and verify the data it - // puts in is correct - model.onDataChange(() => { - for (let i = 0; i < testData.length; i++) { - for (let j = 0; j < testData[i].length; j++) { - equal(testData[i][j], model.rows[i][j]); + controller.update({ query: 'query' }, profile).then(() => { + // Once we update the controller, listen on when it changes the model and verify the data it + // puts in is correct + model.onDataChange(() => { + for (let i = 0; i < testData.length; i++) { + for (let j = 0; j < testData[i].length; j++) { + equal(testData[i][j], model.rows[i][j]); + } } - } - done(); + done(); + }); + // Fake the query Runner telling the controller the query is complete + complete(); }); - // Fake the query Runner telling the controller the query is complete - complete(); + }); }); diff --git a/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts b/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts index 17424989e0..dcc481e541 100644 --- a/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts +++ b/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts @@ -9,6 +9,7 @@ import * as os from 'os'; import { resolveQueryFilePath } from 'sql/workbench/services/insights/common/insightsUtils'; import * as path from 'vs/base/common/path'; +import * as fs from 'fs'; import { Workspace, toWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ConfigurationResolverService, BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; @@ -45,7 +46,9 @@ suite('Insights Utils tests', function () { // Create test file - just needs to exist for verifying the path resolution worked correctly testRootPath = path.join(os.tmpdir(), 'adstests'); queryFileDir = getRandomTestPath(testRootPath, 'insightsutils'); - await pfs.mkdirp(queryFileDir); + + await fs.promises.mkdir(queryFileDir, { recursive: true }); + queryFilePath = path.join(queryFileDir, 'test.sql'); await pfs.writeFile(queryFilePath, ''); }); diff --git a/src/sql/workbench/services/tasks/common/tasksService.ts b/src/sql/workbench/services/tasks/common/tasksService.ts index e8bf31cf4f..a640a084dc 100644 --- a/src/sql/workbench/services/tasks/common/tasksService.ts +++ b/src/sql/workbench/services/tasks/common/tasksService.ts @@ -59,9 +59,7 @@ export class TaskService implements ITaskService { this._taskQueue = new TaskNode('Root'); this._onTaskComplete = new Emitter(); this._onAddNewTask = new Emitter(); - - lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown())); - + lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.taskservice')); } /** diff --git a/src/tsconfig.tsec.json b/src/tsconfig.tsec.json new file mode 100644 index 0000000000..d2524df22d --- /dev/null +++ b/src/tsconfig.tsec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "plugins": [ + { + "name": "tsec", + "exemptionConfig": "./tsec.exemptions.json" + } + ] + }, + "exclude": [ + "*/test/*", + "**/*.test.ts" + ] +} diff --git a/src/tsec.exemptions.json b/src/tsec.exemptions.json new file mode 100644 index 0000000000..dc1e805868 --- /dev/null +++ b/src/tsec.exemptions.json @@ -0,0 +1,5 @@ +{ + "ban-trustedtypes-createpolicy": [ + "**/*.ts" + ] +} diff --git a/src/typings/require.d.ts b/src/typings/require.d.ts index 7dbada717a..7c7db4b411 100644 --- a/src/typings/require.d.ts +++ b/src/typings/require.d.ts @@ -47,6 +47,7 @@ interface NodeRequire { onError: Function; __$__nodeRequire(moduleName: string): T; getStats(): ReadonlyArray; + hasDependencyCycle(): boolean; define(amdModuleId: string, dependencies: string[], callback: (...args: any[]) => any): any; } diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index 4968e1652d..63221b03c2 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -110,13 +110,13 @@ export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscree const userAgent = navigator.userAgent; -export const isEdge = (userAgent.indexOf('Edge/') >= 0); -export const isOpera = (userAgent.indexOf('Opera') >= 0); +export const isEdgeLegacy = (userAgent.indexOf('Edge/') >= 0); export const isFirefox = (userAgent.indexOf('Firefox') >= 0); export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0); export const isChrome = (userAgent.indexOf('Chrome') >= 0); export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0)); export const isWebkitWebView = (!isChrome && !isSafari && isWebKit); export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0)); -export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0); +export const isEdgeLegacyWebView = isEdgeLegacy && (userAgent.indexOf('WebView/') >= 0); +export const isElectron = (userAgent.indexOf('Electron/') >= 0); export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches); diff --git a/src/vs/base/browser/canIUse.ts b/src/vs/base/browser/canIUse.ts index 2d2df8496c..1e81c3fd01 100644 --- a/src/vs/base/browser/canIUse.ts +++ b/src/vs/base/browser/canIUse.ts @@ -27,7 +27,7 @@ export const BrowserFeatures = { || !!(navigator && navigator.clipboard && navigator.clipboard.readText) ), richText: (() => { - if (browser.isEdge) { + if (browser.isEdgeLegacy) { let index = navigator.userAgent.indexOf('Edge/'); let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10); diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index 2b6f3e36a7..76b7d02540 100644 --- a/src/vs/base/browser/contextmenu.ts +++ b/src/vs/base/browser/contextmenu.ts @@ -16,7 +16,7 @@ export interface IContextMenuEvent { export interface IContextMenuDelegate { getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; }; - getActions(): IAction[]; + getActions(): readonly IAction[]; getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox'; getActionViewItem?(action: IAction): IActionViewItem | undefined; getActionsContext?(event?: IContextMenuEvent): any; diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index f2f19b5ea9..543c16d3d1 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -20,7 +20,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; export function clearNode(node: HTMLElement): void { while (node.firstChild) { - node.removeChild(node.firstChild); + node.firstChild.remove(); } } @@ -41,13 +41,7 @@ export function trustedInnerHTML(node: Element, value: TrustedHTML): void { } export function isInDOM(node: Node | null): boolean { - while (node) { - if (node === document.body) { - return true; - } - node = node.parentNode || (node as ShadowRoot).host; - } - return false; + return node?.isConnected ?? false; } interface IDomClassList { @@ -357,7 +351,7 @@ export function modify(callback: () => void): IDisposable { } /** - * Add a throttled listener. `handler` is fired at most every 16ms or with the next animation frame (if browser supports it). + * Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it). */ export interface IEventMerger { (lastEvent: R | null, currentEvent: E): R; @@ -368,7 +362,7 @@ export interface DOMEvent { stopPropagation(): void; } -const MINIMUM_TIME_MS = 16; +const MINIMUM_TIME_MS = 8; const DEFAULT_EVENT_MERGER: IEventMerger = function (lastEvent: DOMEvent | null, currentEvent: DOMEvent) { return currentEvent; }; @@ -521,6 +515,8 @@ export interface IDimension { export class Dimension implements IDimension { + static readonly None = new Dimension(0, 0); + constructor( public readonly width: number, public readonly height: number, @@ -912,7 +908,7 @@ export const EventType = { MOUSE_OUT: 'mouseout', MOUSE_ENTER: 'mouseenter', MOUSE_LEAVE: 'mouseleave', - MOUSE_WHEEL: browser.isEdge ? 'mousewheel' : 'wheel', + MOUSE_WHEEL: browser.isEdgeLegacy ? 'mousewheel' : 'wheel', POINTER_UP: 'pointerup', POINTER_DOWN: 'pointerdown', POINTER_MOVE: 'pointermove', @@ -1072,9 +1068,13 @@ export function after(sibling: HTMLElement, child: T): T { return child; } -export function append(parent: HTMLElement, ...children: T[]): T { - children.forEach(child => parent.appendChild(child)); - return children[children.length - 1]; +export function append(parent: HTMLElement, child: T): T; +export function append(parent: HTMLElement, ...children: (T | string)[]): void; +export function append(parent: HTMLElement, ...children: (T | string)[]): T | void { + parent.append(...children); + if (children.length === 1 && typeof children[0] !== 'string') { + return children[0]; + } } export function prepend(parent: HTMLElement, child: T): T { @@ -1082,26 +1082,12 @@ export function prepend(parent: HTMLElement, child: T): T { return child; } - /** * Removes all children from `parent` and appends `children` */ export function reset(parent: HTMLElement, ...children: Array): void { parent.innerText = ''; - appendChildren(parent, ...children); -} - -/** - * Appends `children` to `parent` - */ -export function appendChildren(parent: HTMLElement, ...children: Array): void { - for (const child of children) { - if (child instanceof Node) { - parent.appendChild(child); - } else if (typeof child === 'string') { - parent.appendChild(document.createTextNode(child)); - } - } + append(parent, ...children); } const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/; @@ -1155,13 +1141,7 @@ function _$(namespace: Namespace, description: string, attrs? } }); - for (const child of children) { - if (child instanceof Node) { - result.appendChild(child); - } else if (typeof child === 'string') { - result.appendChild(document.createTextNode(child)); - } - } + result.append(...children); return result as T; } @@ -1281,9 +1261,10 @@ export function computeScreenAwareSize(cssPx: number): number { * See https://mathiasbynens.github.io/rel-noopener/ */ export function windowOpenNoOpener(url: string): void { - if (platform.isNative || browser.isEdgeWebView) { + if (browser.isElectron || browser.isEdgeLegacyWebView) { // In VSCode, window.open() always returns null... // The same is true for a WebView (see https://github.com/microsoft/monaco-editor/issues/628) + // Also call directly window.open in sandboxed Electron (see https://github.com/microsoft/monaco-editor/issues/2220) window.open(url); } else { let newTab = window.open(); @@ -1316,10 +1297,14 @@ export function asCSSUrl(uri: URI): string { return `url('${FileAccess.asBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`; } +export function asCSSPropertyValue(value: string) { + return `'${value.replace(/'/g, '%27')}'`; +} + export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void { // If the data is provided as Buffer, we create a - // blog URL out of it to produce a valid link + // blob URL out of it to produce a valid link let url: string; if (URI.isUri(dataOrUri)) { url = dataOrUri.toString(true); @@ -1368,7 +1353,7 @@ export interface IDetectedFullscreen { mode: DetectedFullscreenMode; /** - * Wether we know for sure that we are in fullscreen mode or + * Whether we know for sure that we are in fullscreen mode or * it is a guess. */ guess: boolean; @@ -1456,7 +1441,7 @@ export function safeInnerHtml(node: HTMLElement, value: string): void { }, ['class', 'id', 'role', 'tabindex']); const html = _ttpSafeInnerHtml?.createHTML(value, options) ?? insane(value, options); - node.innerHTML = html as unknown as string; + node.innerHTML = html as string; } /** @@ -1469,7 +1454,12 @@ function toBinary(str: string): string { for (let i = 0; i < codeUnits.length; i++) { codeUnits[i] = str.charCodeAt(i); } - return String.fromCharCode(...new Uint8Array(codeUnits.buffer)); + let binary = ''; + const uint8array = new Uint8Array(codeUnits.buffer); + for (let i = 0; i < uint8array.length; i++) { + binary += String.fromCharCode(uint8array[i]); + } + return binary; } /** @@ -1516,7 +1506,7 @@ export namespace WebFileSystemAccess { } export function supported(obj: any & Window): obj is FileSystemAccess { - const candidate = obj as FileSystemAccess; + const candidate = obj as FileSystemAccess | undefined; if (typeof candidate?.showDirectoryPicker === 'function') { return true; } @@ -1554,6 +1544,11 @@ export class ModifierKeyEmitter extends Emitter { }; this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => { + // if keydown event is repeated, ignore it #112347 + if (e.repeat) { + return; + } + const event = new StandardKeyboardEvent(e); if (e.altKey && !this._keyStatus.altKey) { @@ -1666,3 +1661,9 @@ export class ModifierKeyEmitter extends Emitter { this._subscriptions.dispose(); } } + +export function getCookieValue(name: string): string | undefined { + const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531 + + return match ? match.pop() : undefined; +} diff --git a/src/vs/base/browser/event.ts b/src/vs/base/browser/event.ts index 5235aed400..4e20182129 100644 --- a/src/vs/base/browser/event.ts +++ b/src/vs/base/browser/event.ts @@ -31,10 +31,12 @@ export interface CancellableEvent { stopPropagation(): void; } +export function stopEvent(event: T): T { + event.preventDefault(); + event.stopPropagation(); + return event; +} + export function stop(event: BaseEvent): BaseEvent { - return BaseEvent.map(event, e => { - e.preventDefault(); - e.stopPropagation(); - return e; - }); -} \ No newline at end of file + return BaseEvent.map(event, stopEvent); +} diff --git a/src/vs/base/browser/hash.ts b/src/vs/base/browser/hash.ts index 01f61ca580..83ad405029 100644 --- a/src/vs/base/browser/hash.ts +++ b/src/vs/base/browser/hash.ts @@ -10,7 +10,14 @@ export async function sha1Hex(str: string): Promise { // Prefer to use browser's crypto module if (globalThis?.crypto?.subtle) { - const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, VSBuffer.fromString(str).buffer); + + // Careful to use `dontUseNodeBuffer` when passing the + // buffer to the browser `crypto` API. Users reported + // native crashes in certain cases that we could trace + // back to passing node.js `Buffer` around + // (https://github.com/microsoft/vscode/issues/114227) + const buffer = VSBuffer.fromString(str, { dontUseNodeBuffer: true }).buffer; + const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, buffer); return toHexString(hash); } diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index f766d602e8..6bb04ea47c 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -15,10 +15,10 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { escape } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { markdownEscapeEscapedCodicons } from 'vs/base/common/codicons'; +import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels'; import { resolvePath } from 'vs/base/common/resources'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; @@ -156,7 +156,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende }; renderer.paragraph = (text): string => { if (markdown.supportThemeIcons) { - const elements = renderCodicons(text); + const elements = renderLabelWithIcons(text); text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join(''); } return `

${text}

`; @@ -233,7 +233,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende // We always pass the output through insane after this so that we don't rely on // marked for sanitization. markedOptions.sanitizer = (html: string): string => { - const match = markdown.isTrusted ? html.match(/^()|(<\/\s*span>)$/) : undefined; + const match = markdown.isTrusted ? html.match(/^(]+>)|(<\/\s*span>)$/) : undefined; return match ? html : ''; }; markedOptions.sanitize = true; @@ -248,13 +248,13 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } // escape theme icons if (markdown.supportThemeIcons) { - value = markdownEscapeEscapedCodicons(value); + value = markdownEscapeEscapedIcons(value); } const renderedMarkdown = marked.parse(value, markedOptions); // sanitize with insane - element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown); + element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as string; // signal that async code blocks can be now be inserted signalInnerHTML!(); @@ -276,13 +276,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende function sanitizeRenderedMarkdown( options: { isTrusted?: boolean }, renderedMarkdown: string, -): string { +): string | TrustedHTML { const insaneOptions = getInsaneOptions(options); - if (_ttpInsane) { - return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string; - } else { - return insane(renderedMarkdown, insaneOptions); - } + return _ttpInsane?.createHTML(renderedMarkdown, insaneOptions) ?? insane(renderedMarkdown, insaneOptions); } function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions { @@ -317,12 +313,12 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti 'td': ['align'] }, filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean { - if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) { - if (token.attrs['style']) { + if (token.tag === 'span' && options.isTrusted) { + if (token.attrs['style'] && (Object.keys(token.attrs).length === 1)) { return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/); } else if (token.attrs['class']) { // The class should match codicon rendering in src\vs\base\common\codicons.ts - return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/); + return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/); } return false; } diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 38e8fd3829..fe5eae6dc5 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -104,7 +104,3 @@ .monaco-action-bar .action-item.action-dropdown-item > .action-label { margin-right: 1px; } - -.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown { - margin-right: 4px; -} diff --git a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts index e4df188c51..8d28dec4a2 100644 --- a/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts +++ b/src/vs/base/browser/ui/breadcrumbs/breadcrumbsWidget.ts @@ -20,27 +20,6 @@ export abstract class BreadcrumbsItem { abstract render(container: HTMLElement): void; } -export class SimpleBreadcrumbsItem extends BreadcrumbsItem { - - constructor( - readonly text: string, - readonly title: string = text - ) { - super(); - } - - equals(other: this) { - return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title; - } - - render(container: HTMLElement): void { - let node = document.createElement('div'); - node.title = this.title; - node.innerText = this.text; - container.appendChild(node); - } -} - export interface IBreadcrumbsWidgetStyles { breadcrumbsBackground?: Color; breadcrumbsForeground?: Color; @@ -333,7 +312,12 @@ export class BreadcrumbsWidget { private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void { dom.clearNode(container); container.className = ''; - item.render(container); + try { + item.render(container); + } catch (err) { + container.innerText = '<>'; + console.error(err); + } container.tabIndex = -1; container.setAttribute('role', 'listitem'); container.classList.add('monaco-breadcrumb-item'); diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 3848b8b5c4..f8afe7bd4f 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -11,15 +11,15 @@ import { mixin } from 'vs/base/common/objects'; 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 { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; -import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { CSSIcon, Codicon } from 'vs/base/common/codicons'; export interface IButtonOptions extends IButtonStyles { readonly title?: boolean | string; - readonly supportCodicons?: boolean; + readonly supportIcons?: boolean; readonly secondary?: boolean; } @@ -46,7 +46,7 @@ const defaultOptions: IButtonStyles = { export interface IButton extends IDisposable { readonly element: HTMLElement; - readonly onDidClick: BaseEvent; + readonly onDidClick: BaseEvent; label: string; icon: CSSIcon; enabled: boolean; @@ -262,8 +262,8 @@ export class Button extends Disposable implements IButton { set label(value: string) { this._element.classList.add('monaco-text-button'); - if (this.options.supportCodicons) { - reset(this._element, ...renderCodicons(value)); + if (this.options.supportIcons) { + reset(this._element, ...renderLabelWithIcons(value)); } else { this._element.textContent = value; } @@ -278,7 +278,7 @@ export class Button extends Disposable implements IButton { // {{SQL CARBON EDIT}} set icon(icon: CSSIcon) { this.hasIcon = icon !== undefined; - this._element.classList.add(...icon.classNames.split(' ')); + this._element.classList.add(...icon.id.split(' ')); this.applyStyles(); } @@ -317,10 +317,12 @@ export interface IButtonWithDropdownOptions extends IButtonOptions { export class ButtonWithDropdown extends Disposable implements IButton { private readonly button: Button; + private readonly action: Action; private readonly dropdownButton: Button; readonly element: HTMLElement; - readonly onDidClick: BaseEvent; + private readonly _onDidClick = this._register(new Emitter()); + readonly onDidClick = this._onDidClick.event; constructor(container: HTMLElement, options: IButtonWithDropdownOptions) { super(); @@ -330,15 +332,16 @@ export class ButtonWithDropdown extends Disposable implements IButton { container.appendChild(this.element); this.button = this._register(new Button(this.element, options)); - this.onDidClick = this.button.onDidClick; + this._register(this.button.onDidClick(e => this._onDidClick.fire(e))); + this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined))); - this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportCodicons: true })); + this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); this.dropdownButton.element.classList.add('monaco-dropdown-button'); this.dropdownButton.icon = Codicon.dropDownButton; - this._register(this.dropdownButton.onDidClick(() => { + this._register(this.dropdownButton.onDidClick(e => { options.contextMenuProvider.showContextMenu({ getAnchor: () => this.dropdownButton.element, - getActions: () => options.actions, + getActions: () => [this.action, ...options.actions], actionRunner: options.actionRunner, onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false') }); @@ -348,6 +351,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { set label(value: string) { this.button.label = value; + this.action.label = value; } set icon(icon: CSSIcon) { diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index eb9a910bda..41712fd10a 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -101,12 +101,10 @@ export class Checkbox extends Widget { const classes = ['monaco-custom-checkbox']; if (this._opts.icon) { - classes.push(this._opts.icon.classNames); - } else { - classes.push('codicon'); // todo@aeschli: remove once codicon fully adopted + classes.push(...CSSIcon.asClassNameArray(this._opts.icon)); } if (this._opts.actionClassName) { - classes.push(this._opts.actionClassName); + classes.push(...this._opts.actionClassName.split(' ')); } if (this._checked) { classes.push('checked'); @@ -114,7 +112,7 @@ export class Checkbox extends Widget { this.domNode = document.createElement('div'); this.domNode.title = this._opts.title; - this.domNode.className = classes.join(' '); + this.domNode.classList.add(...classes); this.domNode.tabIndex = 0; this.domNode.setAttribute('role', 'checkbox'); this.domNode.setAttribute('aria-checked', String(this._checked)); diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css new file mode 100644 index 0000000000..e4f6736a25 --- /dev/null +++ b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.codicon-wrench-subaction { + opacity: 0.5; +} + +@keyframes codicon-spin { + 100% { + transform:rotate(360deg); + } +} + +.codicon-sync.codicon-modifier-spin, .codicon-loading.codicon-modifier-spin{ + /* Use steps to throttle FPS to reduce CPU usage */ + animation: codicon-spin 1.5s steps(30) infinite; +} + +.codicon-modifier-disabled { + opacity: 0.4; +} + +/* custom speed & easing for loading icon */ +.codicon-loading, +.codicon-tree-item-loading::before { + animation-duration: 1s !important; + animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important; +} diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 7fdf6ed7b9db8580b4a762998936d06b55c37b51..01abb46b868b7211758d3e2b92189072fc12ff08 100644 GIT binary patch delta 7229 zcmZvg34B!bwa0(wPLc`9WU|j>zgZw5A$u~J5XcNW$YK&8As~b-BrFLKLQt>_g9=J5 z1vGBY;!?F1=|+iCOY&Ok^HJJ|idHRct)m9Ld2S2tQ$qC*A&_MuA9h=*F|Iur-0ru%Yf^U7#uCDt2#2)||g}}KB8#>$8 zr5?T%!slLMsy6Te`_Dsbcz>9gxM6eu&dPA!1HBJ~Hur4pXuEdTk1hbIy8+|do7;Bw z8ryJ|zfaNNGnXf$lFZt=H?TYO~9*4uwNBP6Qp{9Wh`YsI*UZDEW`;D}LOnZEK z?)Vj9%Y$5?r82{D7h{m2M_bIN7lk zdH4l7aTL#^l!~wlPFjLpSPzAt;y>^*RpVjYi9=LQDcDF;D4wz@lM*P03TP5#;0PsC z-oSOc894(pF$=RX5Az3d!@a?3)LqZSLX2#awumZ1?%Sb=7T8jHE6+Fw4x2| z=)gL3VFNZ{GqzwG`mi1S*nwMd8+PJ$d;z-=z#as#7kA(;?8AOO=Wg7C1NagS;xO*R z03N_Ycm)53NAZ;)9>e2!0#D+rcnVMB8GH?2$8$K2Z{Y;Ki<20{DSQtv;`=y_f5#7T z20y|}_)lKibNC6);}yJ)x9~Q8j*Ivueua1NYrKo!;CFZ*zsCpo11{r3SokBZ;7|An z*YFpN;IH@?f5RsTj^ZDppry2omQy3Xfp6m5 z_zvb^F6uX-0ZXxf@==H7SOPzik%A#~qZbd-EH;{G+=OTGU-&x};%E3W8AN!E;_xPZ z3m+{+1v^9!?#B!GG2TNO-oUGvjC^>hnC8(8@{u23Wb+Igm>KcOKz&4H(4j3hM!N-n zs0pxI1vXAtZ32$5Y!}!#VRZ;>rm)rtY_PC81?{F*m%v60YrOz=No#|^h7D_DP;dCo z?CaJhfvp`@w}7cEdjz_fCl_rTgNAX#hN zFR)9&+9BZifqI8KSj3~90y`S4T>_jeZx`74V0}SgH-yC@N5c*YDj)1R%3*n*Km{!C7ibd80f9pctOo?>-#sW$GRubq z4m>YL{jhchkG?K&2!f^Ui{W4d>p6kL60G9_2PjzI5HNr6d1oDQtb+9ufuk0z^8&{& zSg!~i$zZ)Ia7=^sn!wQw)&+s%9IV#`j(D)%5IFY1`kBD<0P9VG;~}iK^wRK#LnExW z1rCz1elBphgmqEizzORY0*6pozZ5u_!uplKVHMUp0tZ-FzZN*u!g^OgE6d*q9Daka zF7bfjNDS+@0>@-n?+F~8Vf{|vI1TH4fg?7o-wPbOVSONQ6o>T(f#W%>%K}GsSRV=; zzhPM#ZN1xhM+qGGVf|6y6aedrz{vpCRe{q2tUn2yAYgqYaH@dyXMvLjtZM?N4_JQ@ zIFY~_5jdr=_xzPN3{M2Cj|J>y`8RBKKEpW* z)(wHP6|6CV^A@a6g&?Oe?bhRPGJ_(4(;Ac^aDs!f37qPnLIh5FP@w{+Kd3N)6Cspc z;FJgzE^u;$njqjSEF<`_c>_+EP>}+sPAG@KNfatdz>_SU0w-3eXn|8KRE)sM7AjWY zvgw<;OYaaP~bWQsz~691gcoz+61aZ;3@^G zRN#6As!ZU@1*%-&8V0ID;A#e{avYxjcX`JnaESv|C2+9=fvOg`0D_t-a47^; zBXCg!HBI312x_{(g%VV)z$FuuPvGJS%Fp*#et!BZEN2K@Q9;cVxVD0tC2*AmHCy0% z3u=zQl^4`pfom|Rc>-5sQ1b<@%b*qrT%keTBo=&y0#|ZSO9Zaz zpl%lMDa(3+Y%CiD3Sqfapiq{}1PWuhT%ZXo8}<70h9Y>-B#?vU3W1_nHVdR(Z>2yn zELRB>%kmb15?QVm$i;GvKyH>Tf^J>ZT7gov|F`mhp){6l0;RKT7szDUA&~ZrbplOf z*(p#i%PxT?vs^EbHp&eGX`|dIkT%3knjqg;TVl6B9+o`;)_6np8!jRn+U zfm;lyBLX)YQ1=P&4=hg3DI4O^g90}oP!9>*ia%%8q2Q<+yX)I{epVK-4N8* z1@4QWjtTIwd`{qA3F^4OT@#dE9Y()UP_%?1)9O~v_SeX^y)C& z&OvFF8Qs|F&;6+l!NQ1D5zja>EjJ)P>IQkqHqbj3TMIDMd>zwS|;5_1d$$25V zCHj0!am)iT=VPwLI$|?oOJjYpO|f0E2jddrisPP%I~^Y$KO=rs{E_(M@vp|emyn&X zIALeP>4Xc3U5Q5%&$)743tWA!M_gxJm)%bHJooMHr`&HO8A+wVq$iULlGi2gO+K4a zkg_x7a_WNAyHhWvtx7wdHkLjw{bc&Z3`a(B#&E_ZGsMg_cblioKW27iC1h>LI+^Xu zemiH~#F&Z8CpjiPo7RSeQ8hW1EuH7s>@cFy;yd&d~x~C^2f{1 zm0z#ut+==1gGyKB>dL1o-}hvAmV5Sj&Q>|Ad{ukA4)40^4b{i0-=DgC>fKX6s##w1 z&1nVG_D;Jz?fUfc>AR;7Prq0jURzjuqISf0z<1T(>_6)tov~%cw`aUPvub8@aAw!6 znpvl2y*s;W_USp5bDo-WeQxvIgLBW#E1dVxys`P!^V{d|oB#5HxCPS}^e;GhQ`$|f zH$A(sdf~A}xr_Q1eX_V_@$SW!msBo!e#ylpqjiqDg>_wZ_t%ZoC)M9v|475l4Lci7 zFEy4HENxkO_tKY_-Mj4Eva#h08y$_+jpv%en)WoUJ>GPAMbC=kD@K}Yn(u6Wb!Fzt zg)1Lj`O?afRWYmjR=sh{fm?oh%R9GRU5(Xot9P$HvnFIs{hCMCeAF_r<@UA4+Lu}r zS{Jq+YQ5T4({`}!WZT>AuJ(oP=i9G#oL!f)ZUZoQG<|uzF>UI=wbI$a-0WxyvxnKs zoRr{pxigINDsNepx5`gdl_iv$SC%W=JU+`w*~K9~@>Y3$RPN3ow<{scSW((lN+ku2 z<;f{gsq?azx2|8S2mI0{iX6$w*_54MP(rS09c8_@_O{p8n3Rz|t-ZYW)~UTun=vy? zTMM5Ul~mq1W$pTs)_K|GDXBc*pOmgI;p^t}1(K5;H%_-tOE=A$`u5)2ww879evJ8a zZ}rZtgCCW7?7;}_I=0KUXL4yK@lqXXxmczdF|!t2Sidnyv6>0kSHZ-qP2 zo|ETItniRMmd}aJv*&m!Vwp)fi4}IY{!xo*4rOK@4IKSU4(Z{L=?fU~ra5eyW?)Vr z5EwiB(o6orZQBZM$N5V>Wtb1@uZPW{K;SX`3-f3oK&LXtzG#|s8-G7`F5vU|C-oNg zPGR*2Cr)k6j~RcAZy*b0`~qni)($+-K0Pxu$5WX*h1~W$kJoLlD0jJCiMAXMYsn8( zk(e`ZquuZFd-Jx}74F|(Sl3@t7w0hiLqq&Ou)bra>#>$RZ++qZ!u>^cm4)f%jjhAJ z;bGt4-uCBG=W|Tvdp^rk7tax?*vNL0J%z^0Rip}y9UpLmiIxCv8JX!o=?|;sY?cT9wk8AA5e3Ry}vy|+rqI>-_#x}V; zVFwfG ze$0CD?hF3G*IsF{2SYwn?l>}dx^q#$lbvJJKAfX{**3n_qg48Amsl0868lPpZE?@* zvrTi3Wo{xoorU;EI!T})k%$bmJ_wTtdYh0<>ulJZY;#h-WIu!_v$BVH` zlqq3_?R3-(GBnj3;)xnt#MAde0HL4HNDjXbmC%MsIe9z>p3gN&WKHa!X`^GCj(WUQ zId%=7CzI3LZMK-1nE}(xY%#lgIe?knm%4d7wus}=@bHj7-*0U5`$xO4@WFsSGhklP zCz!q6hbqc-?D4ue-5BgU{HGI;JuI5^IMhd#xp@^C zln`dm$tosKrMJRKLH4VETC0u2frtP2HGOhaXsF#bAvQc=!USiS5o-$#ak^X)tKuR; zL+o*pX=a=wJ|!YH&J`XLoo0j>_E39ROk_w%WN3IqQfi_zEYubf5@v)t!XpwRQrsyC zv?nDkJ*7B3J=H88%g9KpDN0F8O*KvguQ!>d?YQZ?{-CW}LrKmUyDcQl7VSuhicX3Q zo9T>-hz&O!v5Co1j?F3N5{ry*EQ8;XEro1$u=9NteTVE&{&^koXEWXCf~I1`d^F+!xtDla&cEAH4R3* z|G42a8jPjJGGn>XXfzos29JHP(XiV)wr+0UxMeWr!}bujd;P}#qTU@nJw<(;+jewr z?=R};>ul>E+;-)=VPT>DZS6gs1KA@Fc;ota^>!9**x1?E*4MFNSJCzz?Fs+qP;Y0S f^M4;4I5kok?Yy@4DtjtqGX%1S5R$MZAwa?!f`mX=6cI2gBBD}=h#QS! z6m3Uo+FhKn(TLLuW7`g6tAk3@xFOn#G_Cilji`ueiSs|sIHP=Z>Qz$p>Q$Y4&biro zChR*Cc4siO5s<9_I+m>J={@sKdyodK7CxD2$m6t8)xuopDIw0^9;M%pSXI-yr1zzRP zlle0%YHI2?grq~WBi4v-pj6BbM@{o<^!L74#eHIeSff_9pbzo@cy?y^e-G% z_dTM+_(R~s6Rq9)<2@|J{dg9m3w zU08r_EW{%8U@?|p87{^pScz5W#pSpHS7HsW!dhI7Yp@>I;yPT94G3W)HsMBW=I`8u zEw~xC;x_yWcVIj2!rj<`dvPCr9m2oh0X&GE_zia9w|EE-<54`0C-4-W#_zBP&)_-i z#q;=g{2u%8A9x9Wz{~hg9N;(oC%l3G#@qM{j^G`@o z3DcwS7~^8aWQ19wn3gb06%!O@nZn167bzw!%*6_CF)mk3WS9k)@P?d4Y*r{{H_W99 zHH<42GahD@!XV>nC6SDmDXs^YUWL_t^&bD^_jq)r;wphzqqtsRu2Nh%Fl!Y?F znqbx`%N6C{`MnClzZB z%x=Z%1M`$ZQ~$@_c3~lcd0nw2!5ma9PB3pOmMIt~zla43=558&1#?KTh{60>v7Esi zRxE5Ve^D%PFh>-N9n3q<$IctpJ(zbDDn49%$JJw9p)>=N)Pk3V$Fy7hhp`IIi=VK zV7^i82r#D=djrh3id_QcJH>v%74$uCh^+(WjN*p^^Mk@h#7PrOfTKrrVNTM^8lVpD?oS%ui2*dY`<6|5-sELfLf_ks;m>|?OuiX9C$Lb12O zMk;nWShr%ogB_yS`Cx}C{F*U}m&zNk8^T5__C;8aVuyr{QS6nlUd65nJ4|60W2|B) zg^g3}sj%^i-4!-LvCqOLDm>1Q+9oMBUs#`F8;12OHe}dj#nud)qS&Ni1B&e$HdV24 z!=@>=aM*OkW)7R7*w$e)Da0Fgc-Sn(UJskC*!5v^6#GAHP;nA~%~g1rak%1i0Gp>c zC&1<_XEwA2in9Z3q2d&QkS*c?aiD;`KykEyEmj;dU`Hs98?Yscg9q$L#SsK{l;SV~ zTdFvgz?LZvD6r*>}bU~2ewvm;(;Bb zIQziXDNaGKV-@Ei*m}ju2)2Rew+*~{%YTiE;}YyR#lZ=7yy6H2J3(=nf}N;1R>4kE z9I#-U6h|%CX2qckwncILf}O0KK@7H4aU_GCqBxwvPE{P!V5cb#Y_M$#=NP9e5ylxx zT#Pf7gfY%i63#eViJNhb^ZoNihVY<6NfhH;CDDwXN}MG*Pl=cDLM6^JoUbH-u}euJ z;{qi~jNM9nj0=^dIO~5A4@d%xJxWp;7b{6;T%shCajB9V#$`%!881?j$9S<4Cvlf6 zaT50uB~G%gaPa#VCtEL7;$-VeB~G@kQsQLmY9&sBUZ%uJ&|W1@f?lq~Nzf~lI0<^C z5+^~|C~=Z9bQNz%oUB}{gb8V{R^lY(Iwek0UZccG%JoW|M7&mslZe+TX<@uxiIZ>} zlsNgu3wMOXNw|$loP@hUiIZ@f6sLFC8x`kx*v*O)J?u@2vpwt<#VH^5X2p3Q_7=s- zANE#-cE;N{J@5wH4Zz;6xG#X+s^Bc2Un%YtV7Dpm8es2G+&{qHskoDX-L6pIw>BkM z`T~#cQQTd??oixkz}~CS$9SKTy1qA3f+6RVdq8m)0{fuiegt->!ViqUQQV`z?o!;X z!2VXjxu%Dd@G|YgihCJY=XD8hGCr!fzkz+sS(UuuRtNTR#Z3?F6ADh=INt(sBLwSw z3&brE>{E)HBUtAfA#Rgkoo|G=VS;^Haq9&8JH<^D>>h_}Xh;0?!JN*Zf>q5z|rXVFPWpT=@fyBV( zz=_nH)az2uq&218pLRUGEqzV;6X|bcbY@(au`A=1%;e0OnYU!Qvi4+$XSZb^&AB3& z5nP-b$bD}3`aDnG%)E>8p3g^qLH_Fe_X`3AQw#1dL}69own9@>U$nL88(AMjR~(F9}^yvaRIlk=-NLk9=t4(NRsK&X&$Dy{+`~vc$65vZZDBm3zwb z$~ToiTYk2pzG7p=ixsCTb1FM4cU2y$imlpNb*6ftrnY8n&B@Wpqc@E{SzBMbe@y(C z^<%b;IZ)@VYpYvPcYobWW5I5@F(;@uNZP0F3LXwreElBOL^hnfbO)0(@QcQ+qt@wBwIJlAq|a{c5(t&OcO zO^KRv=aj=!6Q^#RdT`p*X}#0#YI~&Zc-vrmQTx{RJ?$r_kD7jRM(m84Gwz@9%8WBJ zGiG+p%9*uj){a@nXFoFg(40kcc6RtW#&>L-+a5l5{M@Z`4|fJSS9b2`JUP!huW?@2 zyq7M_xbVn)-~594wezRW@1Fl!S8Uhvt_QkKFId0eV7IHgsr#|+(+ejqT(NN5!nYQ= z7HwYiSkH!@!;7=}i!){?xaw;9HkD2Y1@j`^!AN(cyVNU*84*?0rB&5c)sb7RM^i&XxI_?kd)SMJM-Ixk3!K6& z*8*p+!T0Cm+w;0|^7HcZD(YPkd3@jJ-?LZpOI+?UpWDYvj?5`>Rp(VDI9J7IWC$;k zPlzbvrRC)D(TXa)oAApcVfxB^ZYWY;oS#RPd>8A@@I-oiX<6R%7$*+?QIZqVQezS# zVv;MecjqLQNn{CPR`{-oegk0&z0lUC)6jz|pb zA0CK_PL7I*iu1<=h7bNeHZ5}K(C{JA*|CXnaf#6t&Up$_8)E(bY=5%UI426kOCwJ+ zEcnZ5g3gJZ4>;VpaeWhZJK~UyBCs3tuTvL@w|{WsNAA$2{F`PpD_AD~?i#ENR+d+m zCzU4!hi6KhJ3BwFJT5!Vc{tmhR6bC@e?R}pj&~w6&)tz(? zIZ6Hg?#%eI6nSUxR@sr2_0tia?(4ytvak_6)4qNC>TBHf4Ox5_nXbe^kL<1KfBx1x z;zGN5E&2Q-&R8@#XZz(`mB9#SfkZN=m?O>?ULEwhO2X=;+Fe#vk(U#h=<}CVRr|R_ z^75(K@xieOhVR(1sziLfh9*j%Zxd(j6E+ydpRE z-t6gPl160Cyl_@#v9E5YJ{Y$9{N>2dveIeo{Zm^u-0t``>(jX;}X=AFYd)_Wq(TcDnlC iwU@a1o4@V}?|=F9p2)tnKi=IpZlI!X^S~|q)c*%UFfil* diff --git a/src/vs/base/browser/ui/codicons/codiconStyles.ts b/src/vs/base/browser/ui/codicons/codiconStyles.ts index 73dea064a8..53137f4a8d 100644 --- a/src/vs/base/browser/ui/codicons/codiconStyles.ts +++ b/src/vs/base/browser/ui/codicons/codiconStyles.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./codicon/codicon'; -import 'vs/css!./codicon/codicon-modifications'; -import 'vs/css!./codicon/codicon-animations'; +import 'vs/css!./codicon/codicon-modifiers'; import { Codicon } from 'vs/base/common/codicons'; diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 2796e3a550..7a49d34595 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -182,7 +182,9 @@ export class Dialog extends Disposable { button.label = mnemonicButtonLabel(buttonMap[index].label, true); this._register(button.onDidClick(e => { - EventHelper.stop(e); + if (e) { + EventHelper.stop(e); + } resolve({ button: buttonMap[index].index, @@ -307,7 +309,9 @@ export class Dialog extends Disposable { } })); - this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray); + const spinModifierClassName = 'codicon-modifier-spin'; + + this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray, spinModifierClassName); switch (this.options.type) { case 'error': @@ -317,7 +321,7 @@ export class Dialog extends Disposable { this.iconElement.classList.add(...dialogWarningIcon.classNamesArray); break; case 'pending': - this.iconElement.classList.add(...Codicon.loading.classNamesArray, 'codicon-animation-spin'); + this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName); break; case 'none': case 'info': diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 0acb019a5a..e87d0e0047 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -201,7 +201,7 @@ export class Dropdown extends BaseDropdown { } export interface IActionProvider { - getActions(): IAction[]; + getActions(): readonly IAction[]; } export interface IDropdownMenuOptions extends IBaseDropdownOptions { @@ -215,7 +215,7 @@ export interface IDropdownMenuOptions extends IBaseDropdownOptions { export class DropdownMenu extends BaseDropdown { private _contextMenuProvider: IContextMenuProvider; private _menuOptions: IMenuOptions | undefined; - private _actions: IAction[] = []; + private _actions: readonly IAction[] = []; private actionProvider?: IActionProvider; private menuClassName: string; private menuAsChild?: boolean; @@ -238,7 +238,7 @@ export class DropdownMenu extends BaseDropdown { return this._menuOptions; } - private get actions(): IAction[] { + private get actions(): readonly IAction[] { if (this.actionProvider) { return this.actionProvider.getActions(); } @@ -246,7 +246,7 @@ export class DropdownMenu extends BaseDropdown { return this._actions; } - private set actions(actions: IAction[]) { + private set actions(actions: readonly IAction[]) { this._actions = actions; } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 83853dc455..f71b23891a 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -170,7 +170,13 @@ export class ActionWithDropdownActionViewItem extends ActionViewItem { super.render(container); if (this.element) { this.element.classList.add('action-dropdown-item'); - this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(this.options).menuActionClassNames || []] }); + const menuActionsProvider = { + getActions: () => { + const actionsProvider = (this.options).menuActionsOrProvider; + return [this._action, ...(Array.isArray(actionsProvider) ? actionsProvider : actionsProvider.getActions())]; + } + }; + this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(this._register(new Action('dropdownAction', undefined)), menuActionsProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(this.options).menuActionClassNames || []] }); this.dropdownMenuActionViewItem.render(this.element); } } diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 4246c82e3d..ea709ee25d 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -258,6 +258,10 @@ export class FindInput extends Widget { this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e)); } + public get onDidChange(): Event { + return this.inputBox.onDidChange; + } + public enable(): void { dom.removeClass(this.domNode, 'disabled'); this.inputBox.enable(); diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index f7618c6ba1..b2ea56df74 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -5,7 +5,7 @@ import * as objects from 'vs/base/common/objects'; import * as dom from 'vs/base/browser/dom'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; export interface IHighlight { start: number; @@ -21,7 +21,7 @@ export class HighlightedLabel { private highlights: IHighlight[] = []; private didEverRender: boolean = false; - constructor(container: HTMLElement, private supportCodicons: boolean) { + constructor(container: HTMLElement, private supportIcons: boolean) { this.domNode = document.createElement('span'); this.domNode.className = 'monaco-highlighted-label'; @@ -61,12 +61,12 @@ export class HighlightedLabel { } if (pos < highlight.start) { const substring = this.text.substring(pos, highlight.start); - children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring])); + children.push(dom.$('span', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring])); pos = highlight.end; } const substring = this.text.substring(highlight.start, highlight.end); - const element = dom.$('span.highlight', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]); + const element = dom.$('span.highlight', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring]); if (highlight.extraClasses) { element.classList.add(highlight.extraClasses); } @@ -76,7 +76,7 @@ export class HighlightedLabel { if (pos < this.text.length) { const substring = this.text.substring(pos,); - children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring])); + children.push(dom.$('span', undefined, ...this.supportIcons ? renderLabelWithIcons(substring) : [substring])); } dom.reset(this.domNode, ...children); diff --git a/src/vs/base/browser/ui/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css index cf32ef0e3a..a11bb47a82 100644 --- a/src/vs/base/browser/ui/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -45,10 +45,13 @@ } .monaco-hover hr { + box-sizing: border-box; + border-left: 0px; + border-right: 0px; margin-top: 4px; margin-bottom: -4px; - margin-left: -10px; - margin-right: -10px; + margin-left: -8px; + margin-right: -8px; height: 1px; } diff --git a/src/vs/base/browser/ui/hover/hoverWidget.ts b/src/vs/base/browser/ui/hover/hoverWidget.ts index 5d77846870..54525b95e9 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.ts +++ b/src/vs/base/browser/ui/hover/hoverWidget.ts @@ -27,7 +27,9 @@ export class HoverWidget extends Disposable { this.contentsDomNode = document.createElement('div'); this.contentsDomNode.className = 'monaco-hover-content'; - this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {})); + this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, { + consumeMouseWheelIfScrollbarIsNeeded: true + })); this.containerDomNode.appendChild(this._scrollbar.getDomNode()); } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index aae2cb0ca2..ac38e6a5ac 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -14,18 +14,20 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { isString } from 'vs/base/common/types'; +import { isFunction, isString } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; +import { localize } from 'vs/nls'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; supportDescriptionHighlights?: boolean; - supportCodicons?: boolean; + supportIcons?: boolean; hoverDelegate?: IHoverDelegate; } export interface IIconLabelMarkdownString { - markdown: IMarkdownString | string | undefined | Promise; + markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise); markdownNotSupportedFallback: string | undefined; } @@ -114,13 +116,13 @@ export class IconLabel extends Disposable { this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container')))); if (options?.supportHighlights) { - this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons); + this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportIcons); } else { this.nameNode = new Label(nameContainer); } if (options?.supportDescriptionHighlights) { - this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons); + this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportIcons); } else { this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); } @@ -190,24 +192,60 @@ export class IconLabel extends Disposable { } } + private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { + if (hoverOptions && isHovering) { + if (mouseX !== undefined) { + (hoverOptions.target).x = mouseX + 10; + } + return hoverDelegate.showHover(hoverOptions); + } + return undefined; + } + + private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise { + if (isString(markdownTooltip)) { + return async () => markdownTooltip; + } else if (isFunction(markdownTooltip.markdown)) { + return markdownTooltip.markdown; + } else { + const markdown = markdownTooltip.markdown; + return async () => markdown; + } + } + private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void { + htmlElement.setAttribute('title', ''); htmlElement.removeAttribute('title'); - let tooltip = isString(markdownTooltip) ? markdownTooltip : markdownTooltip.markdown; + let tooltip = this.getTooltipForCustom(markdownTooltip); + // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. // On Mac, the delay is 1500. const hoverDelay = isMacintosh ? 1500 : 500; let hoverOptions: IHoverDelegateOptions | undefined; let mouseX: number | undefined; + let isHovering = false; + let tokenSource: CancellationTokenSource; function mouseOver(this: HTMLElement, e: MouseEvent): any { - let isHovering = true; - function mouseMove(this: HTMLElement, e: MouseEvent): any { - mouseX = e.x; + if (isHovering) { + return; } + tokenSource = new CancellationTokenSource(); function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any { - isHovering = false; + if ((e).fromElement === htmlElement) { + isHovering = false; + hoverOptions = undefined; + tokenSource.dispose(true); + mouseLeaveDisposable.dispose(); + mouseDownDisposable.dispose(); + } } const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); + isHovering = true; + + function mouseMove(this: HTMLElement, e: MouseEvent): any { + mouseX = e.x; + } const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); setTimeout(async () => { if (isHovering && tooltip) { @@ -217,25 +255,29 @@ export class IconLabel extends Disposable { targetElements: [this], dispose: () => { } }; - const resolvedTooltip = await tooltip; + hoverOptions = { + text: localize('iconLabel.loading', "Loading..."), + target, + anchorPosition: AnchorPosition.BELOW + }; + const hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + + const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined); if (resolvedTooltip) { hoverOptions = { text: resolvedTooltip, target, anchorPosition: AnchorPosition.BELOW }; + // awaiting the tooltip could take a while. Make sure we're still hovering. + IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering); + } else if (hoverDisposable) { + hoverDisposable.dispose(); } } - if (hoverOptions) { - if (mouseX !== undefined) { - (hoverOptions.target).x = mouseX + 10; - } - hoverDelegate.showHover(hoverOptions); - } + } mouseMoveDisposable.dispose(); - mouseLeaveDisposable.dispose(); - mouseDownDisposable.dispose(); }, hoverDelay); } const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); @@ -322,7 +364,7 @@ class LabelWithHighlights { private singleLabel: HighlightedLabel | undefined = undefined; private options: IIconLabelValueOptions | undefined; - constructor(private container: HTMLElement, private supportCodicons: boolean) { } + constructor(private container: HTMLElement, private supportIcons: boolean) { } setLabel(label: string | string[], options?: IIconLabelValueOptions): void { if (this.label === label && equals(this.options, options)) { @@ -336,7 +378,7 @@ class LabelWithHighlights { if (!this.singleLabel) { this.container.innerText = ''; this.container.classList.remove('multiple'); - this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons); + this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportIcons); } this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines); @@ -354,7 +396,7 @@ class LabelWithHighlights { const id = options?.domId && `${options?.domId}_${i}`; const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }); - const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons); + const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportIcons); highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines); if (i < label.length - 1) { diff --git a/src/vs/base/browser/codicons.ts b/src/vs/base/browser/ui/iconLabel/iconLabels.ts similarity index 56% rename from src/vs/base/browser/codicons.ts rename to src/vs/base/browser/ui/iconLabel/iconLabels.ts index d0579463ee..001d1fe956 100644 --- a/src/vs/base/browser/codicons.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabels.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { CSSIcon } from 'vs/base/common/codicons'; -const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi; - -export function renderCodicons(text: string): Array { +const labelWithIconsRegex = new RegExp(`(\\\\)?\\$\\((${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?)\\)`, 'g'); +export function renderLabelWithIcons(text: string): Array { const elements = new Array(); let match: RegExpMatchArray | null; let textStart = 0, textStop = 0; - while ((match = renderCodiconsRegex.exec(text)) !== null) { + while ((match = labelWithIconsRegex.exec(text)) !== null) { textStop = match.index || 0; elements.push(text.substring(textStart, textStop)); textStart = (match.index || 0) + match[0].length; - const [, escaped, codicon, name, animation] = match; - elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation)); + const [, escaped, codicon] = match; + elements.push(escaped ? `$(${codicon})` : renderIcon({ id: codicon })); } if (textStart < text.length) { @@ -27,6 +27,8 @@ export function renderCodicons(text: string): Array { return elements; } -export function renderCodicon(name: string, animation: string): HTMLSpanElement { - return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`); +export function renderIcon(icon: CSSIcon): HTMLSpanElement { + const node = dom.$(`span`); + node.classList.add(...CSSIcon.asClassNameArray(icon)); + return node; } diff --git a/src/vs/base/browser/ui/iconLabel/iconlabel.css b/src/vs/base/browser/ui/iconLabel/iconlabel.css index 8aea516702..abac1d6638 100644 --- a/src/vs/base/browser/ui/iconLabel/iconlabel.css +++ b/src/vs/base/browser/ui/iconLabel/iconlabel.css @@ -55,6 +55,10 @@ white-space: pre; /* enable to show labels that include multiple whitespaces */ } +.monaco-icon-label.nowrap > .monaco-icon-label-container > .monaco-icon-description-container > .label-description{ + white-space: nowrap +} + .vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description { opacity: .95; } @@ -64,6 +68,16 @@ font-style: italic; } +.monaco-icon-label.deprecated { + text-decoration: line-through; + opacity: 0.66; +} + +/* make sure apply italic font style to decorations as well */ +.monaco-icon-label.italic::after { + font-style: italic; +} + .monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name, .monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description { text-decoration: line-through; @@ -77,10 +91,6 @@ text-align: center; } -.monaco-icon-label.italic::after { - font-style: italic; -} - /* make sure selection color wins when a label is being selected */ .monaco-list:focus .selected .monaco-icon-label, /* list */ .monaco-list:focus .selected .monaco-icon-label::after diff --git a/src/vs/base/browser/ui/codicons/codiconLabel.ts b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts similarity index 76% rename from src/vs/base/browser/ui/codicons/codiconLabel.ts rename to src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts index 8141a44925..5a13f25bab 100644 --- a/src/vs/base/browser/ui/codicons/codiconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/simpleIconLabel.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { reset } from 'vs/base/browser/dom'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -export class CodiconLabel { +export class SimpleIconLabel { constructor( private readonly _container: HTMLElement ) { } set text(text: string) { - reset(this._container, ...renderCodicons(text ?? '')); + reset(this._container, ...renderLabelWithIcons(text ?? '')); } set title(title: string) { diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index d7bcfe6669..49fae20a71 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -17,20 +17,20 @@ border: 1px solid transparent; } -.monaco-inputbox > .wrapper > .input, -.monaco-inputbox > .wrapper > .mirror { +.monaco-inputbox > .ibwrapper > .input, +.monaco-inputbox > .ibwrapper > .mirror { /* Customizable */ padding: 4px; } -.monaco-inputbox > .wrapper { +.monaco-inputbox > .ibwrapper { position: relative; width: 100%; height: 100%; } -.monaco-inputbox > .wrapper > .input { +.monaco-inputbox > .ibwrapper > .input { display: inline-block; box-sizing: border-box; width: 100%; @@ -43,26 +43,26 @@ color: inherit; } -.monaco-inputbox > .wrapper > input { +.monaco-inputbox > .ibwrapper > input { text-overflow: ellipsis; } -.monaco-inputbox > .wrapper > textarea.input { +.monaco-inputbox > .ibwrapper > textarea.input { display: block; -ms-overflow-style: none; /* IE 10+: hide scrollbars */ scrollbar-width: none; /* Firefox: hide scrollbars */ outline: none; } -.monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar { +.monaco-inputbox > .ibwrapper > textarea.input::-webkit-scrollbar { display: none; /* Chrome + Safari: hide scrollbar */ } -.monaco-inputbox > .wrapper > textarea.input.empty { +.monaco-inputbox > .ibwrapper > textarea.input.empty { white-space: nowrap; } -.monaco-inputbox > .wrapper > .mirror { +.monaco-inputbox > .ibwrapper > .mirror { position: absolute; display: inline-block; width: 100%; @@ -89,7 +89,6 @@ padding: 0.4em; font-size: 12px; line-height: 17px; - min-height: 34px; margin-top: -1px; word-wrap: break-word; } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e09346c7b1..bf459bdf75 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -169,7 +169,7 @@ export class InputBox extends Widget { let tagName = this.options.flexibleHeight ? 'textarea' : 'input'; - let wrapper = dom.append(this.element, $('.wrapper')); + let wrapper = dom.append(this.element, $('.ibwrapper')); this.input = dom.append(wrapper, $(tagName + '.input.empty')); this.input.setAttribute('autocorrect', 'off'); this.input.setAttribute('autocapitalize', 'off'); @@ -316,6 +316,9 @@ export class InputBox extends Widget { if (range) { this.input.setSelectionRange(range.start, range.end); + if (range.end === this.input.value.length) { + this.input.scrollLeft = this.input.scrollWidth; + } } } @@ -422,7 +425,7 @@ export class InputBox extends Widget { return !!this.validation && !this.validation(this.value); } - public validate(): boolean { + public validate(): boolean { // {{SQL CARBON EDIT}} let errorMsg: IMessage | null = null; if (this.validation) { @@ -446,7 +449,7 @@ export class InputBox extends Widget { } } - // {{SQL CARBON EDIT}} Canidate for addition to vscode + // {{SQL CARBON EDIT}} Candidate for addition to vscode return errorMsg ? errorMsg.type !== MessageType.ERROR : true; } diff --git a/src/vs/base/browser/ui/list/list.css b/src/vs/base/browser/ui/list/list.css index 446d52293a..506318965e 100644 --- a/src/vs/base/browser/ui/list/list.css +++ b/src/vs/base/browser/ui/list/list.css @@ -33,7 +33,7 @@ .monaco-list-row { position: absolute; - box-sizing: border-box; + box-sizing: border-box; overflow: hidden; width: 100%; } @@ -49,7 +49,9 @@ } /* Focus */ -.monaco-list.element-focused, .monaco-list.selection-single, .monaco-list.selection-multiple { +.monaco-list.element-focused, +.monaco-list.selection-single, +.monaco-list.selection-multiple { outline: 0 !important; } @@ -64,6 +66,7 @@ border-radius: 10px; font-size: 12px; position: absolute; + z-index: 1000; } /* Type filter */ diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 79f4c412f3..1decb61c01 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -258,6 +258,10 @@ export class PagedList implements IThemable, IDisposable { return this.list.getSelection(); } + getSelectedElements(): T[] { + return this.getSelection().map(i => this.model.get(i)); + } + layout(height?: number, width?: number): void { this.list.layout(height, width); } diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index ca4b5bc348..6ff19e4abd 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -64,6 +64,7 @@ export interface IListViewOptions extends IListViewOptionsUpdate { readonly mouseSupport?: boolean; readonly accessibilityProvider?: IListViewAccessibilityProvider; readonly transformOptimization?: boolean; + readonly alwaysConsumeMouseWheel?: boolean; } const DefaultOptions = { @@ -80,7 +81,8 @@ const DefaultOptions = { drop() { } }, horizontalScrolling: false, - transformOptimization: true + transformOptimization: true, + alwaysConsumeMouseWheel: true, }; export class ElementsDragAndDropData implements IDragAndDropData { @@ -327,6 +329,7 @@ export class ListView implements ISpliceable, IDisposable { this.scrollable = new Scrollable(getOrDefault(options, o => o.smoothScrolling, false) ? 125 : 0, cb => scheduleAtNextAnimationFrame(cb)); this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, { + alwaysConsumeMouseWheel: getOrDefault(options, o => o.alwaysConsumeMouseWheel, DefaultOptions.alwaysConsumeMouseWheel), horizontal: ScrollbarVisibility.Auto, vertical: getOrDefault(options, o => o.verticalScrollMode, DefaultOptions.verticalScrollMode), useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows), @@ -432,8 +435,24 @@ export class ListView implements ISpliceable, IDisposable { const deleteRange = { start, end: start + deleteCount }; const removeRange = Range.intersect(previousRenderRange, deleteRange); + // try to reuse rows, avoid removing them from DOM + const rowsToDispose = new Map(); for (let i = removeRange.start; i < removeRange.end; i++) { - this.removeItemFromDOM(i); + const item = this.items[i]; + item.dragStartDisposable.dispose(); + + if (item.row) { + let rows = rowsToDispose.get(item.templateId); + + if (!rows) { + rows = []; + rowsToDispose.set(item.templateId, rows); + } + + rows.push([item.row, item.element, i, item.size]); + } + + item.row = null; } const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length }; @@ -491,7 +510,34 @@ export class ListView implements ISpliceable, IDisposable { for (const range of insertRanges) { for (let i = range.start; i < range.end; i++) { - this.insertItemInDOM(i, beforeElement); + const item = this.items[i]; + const rows = rowsToDispose.get(item.templateId); + const rowData = rows?.pop(); + + if (!rowData) { + this.insertItemInDOM(i, beforeElement); + } else { + const [row, element, index, size] = rowData; + const renderer = this.renderers.get(item.templateId); + + if (renderer && renderer.disposeElement) { + renderer.disposeElement(element, index, row.templateData, size); + } + + this.insertItemInDOM(i, beforeElement, row); + } + } + } + + for (const [templateId, rows] of rowsToDispose) { + for (const [row, element, index, size] of rows) { + const renderer = this.renderers.get(templateId); + + if (renderer && renderer.disposeElement) { + renderer.disposeElement(element, index, row.templateData, size); + } + + this.cache.release(row); } } @@ -699,24 +745,26 @@ export class ListView implements ISpliceable, IDisposable { // DOM operations - private insertItemInDOM(index: number, beforeElement: HTMLElement | null): void { + private insertItemInDOM(index: number, beforeElement: HTMLElement | null, row?: IRow): void { const item = this.items[index]; if (!item.row) { - item.row = this.cache.alloc(item.templateId); - const role = this.accessibilityProvider.getRole(item.element) || 'listitem'; - item.row!.domNode!.setAttribute('role', role); - const checked = this.accessibilityProvider.isChecked(item.element); - if (typeof checked !== 'undefined') { - item.row!.domNode!.setAttribute('aria-checked', String(!!checked)); - } + item.row = row ?? this.cache.alloc(item.templateId); } - if (!item.row.domNode!.parentElement) { + const role = this.accessibilityProvider.getRole(item.element) || 'listitem'; + item.row.domNode.setAttribute('role', role); + + const checked = this.accessibilityProvider.isChecked(item.element); + if (typeof checked !== 'undefined') { + item.row.domNode.setAttribute('aria-checked', String(!!checked)); + } + + if (!item.row.domNode.parentElement) { if (beforeElement) { - this.rowsContainer.insertBefore(item.row.domNode!, beforeElement); + this.rowsContainer.insertBefore(item.row.domNode, beforeElement); } else { - this.rowsContainer.appendChild(item.row.domNode!); + this.rowsContainer.appendChild(item.row.domNode); } } @@ -734,10 +782,10 @@ export class ListView implements ISpliceable, IDisposable { const uri = this.dnd.getDragURI(item.element); item.dragStartDisposable.dispose(); - item.row.domNode!.draggable = !!uri; + item.row.domNode.draggable = !!uri; if (uri) { - const onDragStart = domEvent(item.row.domNode!, 'dragstart'); + const onDragStart = domEvent(item.row.domNode, 'dragstart'); item.dragStartDisposable = onDragStart(event => this.onDragStart(item.element, uri, event)); } @@ -768,36 +816,39 @@ export class ListView implements ISpliceable, IDisposable { } private updateItemInDOM(item: IItem, index: number): void { - item.row!.domNode!.style.top = `${this.elementTop(index)}px`; + item.row!.domNode.style.top = `${this.elementTop(index)}px`; if (this.setRowHeight) { - item.row!.domNode!.style.height = `${item.size}px`; + item.row!.domNode.style.height = `${item.size}px`; } if (this.setRowLineHeight) { - item.row!.domNode!.style.lineHeight = `${item.size}px`; + item.row!.domNode.style.lineHeight = `${item.size}px`; } - item.row!.domNode!.setAttribute('data-index', `${index}`); - item.row!.domNode!.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false'); - item.row!.domNode!.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length))); - item.row!.domNode!.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index))); - item.row!.domNode!.setAttribute('id', this.getElementDomId(index)); + item.row!.domNode.setAttribute('data-index', `${index}`); + item.row!.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false'); + item.row!.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length))); + item.row!.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index))); + item.row!.domNode.setAttribute('id', this.getElementDomId(index)); - item.row!.domNode!.classList.toggle('drop-target', item.dropTarget); + item.row!.domNode.classList.toggle('drop-target', item.dropTarget); } private removeItemFromDOM(index: number): void { const item = this.items[index]; item.dragStartDisposable.dispose(); - const renderer = this.renderers.get(item.templateId); - if (item.row && renderer && renderer.disposeElement) { - renderer.disposeElement(item.element, index, item.row.templateData, item.size); - } + if (item.row) { + const renderer = this.renderers.get(item.templateId); - this.cache.release(item.row!); - item.row = null; + if (renderer && renderer.disposeElement) { + renderer.disposeElement(item.element, index, item.row.templateData, item.size); + } + + this.cache.release(item.row); + item.row = null; + } if (this.horizontalScrolling) { this.eventuallyUpdateScrollWidth(); @@ -809,14 +860,14 @@ export class ListView implements ISpliceable, IDisposable { return scrollPosition.scrollTop; } - setScrollTop(scrollTop: number): void { + setScrollTop(scrollTop: number, reuseAnimation?: boolean): void { if (this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable.dispose(); this.scrollableElementUpdateDisposable = null; this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); } - this.scrollableElement.setScrollPosition({ scrollTop }); + this.scrollableElement.setScrollPosition({ scrollTop, reuseAnimation }); } getScrollLeft(): number { @@ -895,9 +946,7 @@ export class ListView implements ISpliceable, IDisposable { this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth); if (this.supportDynamicHeights) { - // Don't update scrollTop from within an scroll event - // so we don't break smooth scrolling. #104144 - this._rerender(e.scrollTop, e.height, false); + this._rerender(e.scrollTop, e.height, e.inSmoothScrolling); } } catch (err) { console.error('Got bad scroll event:', e); @@ -1027,7 +1076,7 @@ export class ListView implements ISpliceable, IDisposable { const item = this.items[index]!; item.dropTarget = true; - if (item.row && item.row.domNode) { + if (item.row) { item.row.domNode.classList.add('drop-target'); } } @@ -1037,7 +1086,7 @@ export class ListView implements ISpliceable, IDisposable { const item = this.items[index]!; item.dropTarget = false; - if (item.row && item.row.domNode) { + if (item.row) { item.row.domNode.classList.remove('drop-target'); } } @@ -1167,7 +1216,7 @@ export class ListView implements ISpliceable, IDisposable { * Given a stable rendered state, checks every rendered element whether it needs * to be probed for dynamic height. Adjusts scroll height and top if necessary. */ - private _rerender(renderTop: number, renderHeight: number, updateScrollTop: boolean = true): void { + private _rerender(renderTop: number, renderHeight: number, inSmoothScrolling?: boolean): void { const previousRenderRange = this.getRenderRange(renderTop, renderHeight); // Let's remember the second element's position, this helps in scrolling up @@ -1233,8 +1282,15 @@ export class ListView implements ISpliceable, IDisposable { } } - if (updateScrollTop && typeof anchorElementIndex === 'number') { - this.scrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta!; + if (typeof anchorElementIndex === 'number') { + // To compute a destination scroll top, we need to take into account the current smooth scrolling + // animation, and then reuse it with a new target (to avoid prolonging the scroll) + // See https://github.com/microsoft/vscode/issues/104144 + // See https://github.com/microsoft/vscode/pull/104284 + // See https://github.com/microsoft/vscode/issues/107704 + const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop; + const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop; + this.setScrollTop(newScrollTop, inSmoothScrolling); } this._onDidChangeContentHeight.fire(this.contentHeight); @@ -1256,7 +1312,7 @@ export class ListView implements ISpliceable, IDisposable { const size = item.size; - if (!this.setRowHeight && item.row && item.row.domNode) { + if (!this.setRowHeight && item.row) { let newSize = item.row.domNode.offsetHeight; item.size = newSize; item.lastDynamicHeightWidth = this.renderWidth; @@ -1265,8 +1321,8 @@ export class ListView implements ISpliceable, IDisposable { const row = this.cache.alloc(item.templateId); - row.domNode!.style.height = ''; - this.rowsContainer.appendChild(row.domNode!); + row.domNode.style.height = ''; + this.rowsContainer.appendChild(row.domNode); const renderer = this.renderers.get(item.templateId); if (renderer) { @@ -1277,14 +1333,14 @@ export class ListView implements ISpliceable, IDisposable { } } - item.size = row.domNode!.offsetHeight; + item.size = row.domNode.offsetHeight; if (this.virtualDelegate.setDynamicHeight) { this.virtualDelegate.setDynamicHeight(item.element, item.size); } item.lastDynamicHeightWidth = this.renderWidth; - this.rowsContainer.removeChild(row.domNode!); + this.rowsContainer.removeChild(row.domNode); this.cache.release(row); return item.size - size; diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 82d546a066..10cd843e42 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -13,7 +13,7 @@ import { Gesture } from 'vs/base/browser/touch'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Event, Emitter, EventBufferer } from 'vs/base/common/event'; -import { domEvent } from 'vs/base/browser/event'; +import { domEvent, stopEvent } from 'vs/base/browser/event'; import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListError, IKeyboardNavigationDelegate } from './list'; import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider, IListViewOptionsUpdate } from './listView'; import { Color } from 'vs/base/common/color'; @@ -854,6 +854,7 @@ export interface IListOptions { readonly additionalScrollHeight?: number; readonly transformOptimization?: boolean; readonly smoothScrolling?: boolean; + readonly alwaysConsumeMouseWheel?: boolean; } export interface IListStyles { @@ -1148,35 +1149,26 @@ export class List implements ISpliceable, IThemable, IDisposable { get onTouchStart(): Event> { return this.view.onTouchStart; } get onTap(): Event> { return this.view.onTap; } - private didJustPressContextMenuKey: boolean = false; @memoize get onContextMenu(): Event> { - const fromKeydown = Event.chain(domEvent(this.view.domNode, 'keydown')) + const fromKeyboard = Event.chain(domEvent(this.view.domNode, 'keyup')) .map(e => new StandardKeyboardEvent(e)) - .filter(e => this.didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10)) - .filter(e => { e.preventDefault(); e.stopPropagation(); return false; }) - .event as Event; - - const fromKeyup = Event.chain(domEvent(this.view.domNode, 'keyup')) - .filter(() => { - const didJustPressContextMenuKey = this.didJustPressContextMenuKey; - this.didJustPressContextMenuKey = false; - return didJustPressContextMenuKey; - }) - .filter(() => this.getFocus().length > 0 && !!this.view.domElement(this.getFocus()[0])) - .map(browserEvent => { - const index = this.getFocus()[0]; - const element = this.view.element(index); - const anchor = this.view.domElement(index) as HTMLElement; + .filter(e => e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10)) + .map(stopEvent) + .map(({ browserEvent }) => { + const focus = this.getFocus(); + const index = focus.length ? focus[0] : undefined; + const element = typeof index !== 'undefined' ? this.view.element(index) : undefined; + const anchor = typeof index !== 'undefined' ? this.view.domElement(index) as HTMLElement : this.view.domNode; return { index, element, anchor, browserEvent }; }) .event; const fromMouse = Event.chain(this.view.onContextMenu) - .filter(() => !this.didJustPressContextMenuKey) + .filter(e => !(e.browserEvent.button === 0 && e.browserEvent.buttons === 0 && !e.browserEvent.ctrlKey && !e.browserEvent.altKey && !e.browserEvent.metaKey)) .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent })) .event; - return Event.any>(fromKeydown, fromKeyup, fromMouse); + return Event.any>(fromKeyboard, fromMouse); } get onKeyDown(): Event { return domEvent(this.view.domNode, 'keydown'); } @@ -1380,7 +1372,7 @@ export class List implements ISpliceable, IThemable, IDisposable { } domFocus(): void { - this.view.domNode.focus(); + this.view.domNode.focus({ preventScroll: true }); } layout(height?: number, width?: number): void { diff --git a/src/vs/base/browser/ui/list/rowCache.ts b/src/vs/base/browser/ui/list/rowCache.ts index b69cba6dfa..68b5f6ea0d 100644 --- a/src/vs/base/browser/ui/list/rowCache.ts +++ b/src/vs/base/browser/ui/list/rowCache.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { $ } from 'vs/base/browser/dom'; export interface IRow { - domNode: HTMLElement | null; + domNode: HTMLElement; templateId: string; templateData: any; } @@ -84,7 +84,6 @@ export class RowCache implements IDisposable { for (const cachedRow of cachedRows) { const renderer = this.getRenderer(templateId); renderer.disposeTemplate(cachedRow.templateData); - cachedRow.domNode = null; cachedRow.templateData = null; } }); diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 70b0596d83..85bacb09b9 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -18,11 +18,12 @@ import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; import { Event } from 'vs/base/common/event'; import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; -import { Codicon, registerCodicon, stripCodicons } from 'vs/base/common/codicons'; +import { Codicon, registerCodicon } from 'vs/base/common/codicons'; import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles'; import { isFirefox } from 'vs/base/browser/browser'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { stripIcons } from 'vs/base/common/iconLabels'; export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/; export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g; @@ -532,7 +533,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem { if (this.options.label) { clearNode(this.label); - let label = stripCodicons(this.getAction().label); + let label = stripIcons(this.getAction().label); if (label) { const cleanLabel = cleanMnemonic(label); if (!this.options.enableMnemonics) { diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index eaf2639374..e614ee6541 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -126,9 +126,11 @@ export class MenuBar extends Disposable { let eventHandled = true; const key = !!e.key ? e.key.toLocaleLowerCase() : ''; - if (event.equals(KeyCode.LeftArrow) || (isMacintosh && event.equals(KeyCode.Tab | KeyMod.Shift))) { + const tabNav = isMacintosh && this.options.compactMode === undefined; + + if (event.equals(KeyCode.LeftArrow) || (tabNav && event.equals(KeyCode.Tab | KeyMod.Shift))) { this.focusPrevious(); - } else if (event.equals(KeyCode.RightArrow) || (isMacintosh && event.equals(KeyCode.Tab))) { + } else if (event.equals(KeyCode.RightArrow) || (tabNav && event.equals(KeyCode.Tab))) { this.focusNext(); } else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) { this.setUnfocusedState(); diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index b9a155853d..24b9bbae25 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -83,7 +83,7 @@ export abstract class AbstractScrollbar extends Widget { * Creates the dom node for an arrow & adds it to the container */ protected _createArrow(opts: ScrollbarArrowOptions): void { - let arrow = this._register(new ScrollbarArrow(opts)); + const arrow = this._register(new ScrollbarArrow(opts)); this.domNode.domNode.appendChild(arrow.bgDomNode); this.domNode.domNode.appendChild(arrow.domNode); } @@ -186,10 +186,10 @@ export abstract class AbstractScrollbar extends Widget { } public delegateMouseDown(e: IMouseEvent): void { - let domTop = this.domNode.domNode.getClientRects()[0].top; - let sliderStart = domTop + this._scrollbarState.getSliderPosition(); - let sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize(); - let mousePos = this._sliderMousePosition(e); + const domTop = this.domNode.domNode.getClientRects()[0].top; + const sliderStart = domTop + this._scrollbarState.getSliderPosition(); + const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize(); + const mousePos = this._sliderMousePosition(e); if (sliderStart <= mousePos && mousePos <= sliderStop) { // Act as if it was a mouse down on the slider if (e.leftButton) { @@ -263,7 +263,7 @@ export abstract class AbstractScrollbar extends Widget { private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void { - let desiredScrollPosition: INewScrollPosition = {}; + const desiredScrollPosition: INewScrollPosition = {}; this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition); this._scrollable.setScrollPositionNow(desiredScrollPosition); @@ -278,6 +278,10 @@ export abstract class AbstractScrollbar extends Widget { } } + public isNeeded(): boolean { + return this._scrollbarState.isNeeded(); + } + // ----------------- Overwrite these protected abstract _renderDomNode(largeSize: number, smallSize: number): void; diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts index c685d326ce..6571b649bd 100644 --- a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts @@ -38,8 +38,8 @@ export class HorizontalScrollbar extends AbstractScrollbar { }); if (options.horizontalHasArrows) { - let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2; - let scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2; + const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2; + const scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2; this._createArrow({ className: 'scra', diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 2ee3e1632d..fe5848048b 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -187,7 +187,7 @@ export abstract class AbstractScrollableElement extends Widget { this._onScroll.fire(e); })); - let scrollbarHost: ScrollbarHost = { + const scrollbarHost: ScrollbarHost = { onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent), onDragStart: () => this._onDragStart(), onDragEnd: () => this._onDragEnd(), @@ -325,7 +325,7 @@ export abstract class AbstractScrollableElement extends Widget { // -------------------- mouse wheel scrolling -------------------- private _setListeningToMouseWheel(shouldListen: boolean): void { - let isListening = (this._mouseWheelToDispose.length > 0); + const isListening = (this._mouseWheelToDispose.length > 0); if (isListening === shouldListen) { // No change @@ -337,7 +337,7 @@ export abstract class AbstractScrollableElement extends Widget { // Start listening (if necessary) if (shouldListen) { - let onMouseWheel = (browserEvent: IMouseWheelEvent) => { + const onMouseWheel = (browserEvent: IMouseWheelEvent) => { this._onMouseWheel(new StandardWheelEvent(browserEvent)); }; @@ -426,7 +426,15 @@ export abstract class AbstractScrollableElement extends Widget { } } - if (this._options.alwaysConsumeMouseWheel || didScroll) { + let consumeMouseWheel = didScroll; + if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) { + consumeMouseWheel = true; + } + if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) { + consumeMouseWheel = true; + } + + if (consumeMouseWheel) { e.preventDefault(); e.stopPropagation(); } @@ -473,8 +481,8 @@ export abstract class AbstractScrollableElement extends Widget { if (this._options.useShadows) { const scrollState = this._scrollable.getCurrentScrollPosition(); - let enableTop = scrollState.scrollTop > 0; - let enableLeft = scrollState.scrollLeft > 0; + const enableTop = scrollState.scrollTop > 0; + const enableLeft = scrollState.scrollLeft > 0; this._leftShadowDomNode!.setClassName('shadow' + (enableLeft ? ' left' : '')); this._topShadowDomNode!.setClassName('shadow' + (enableTop ? ' top' : '')); @@ -549,8 +557,12 @@ export class SmoothScrollableElement extends AbstractScrollableElement { super(element, options, scrollable); } - public setScrollPosition(update: INewScrollPosition): void { - this._scrollable.setScrollPositionNow(update); + public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void { + if (update.reuseAnimation) { + this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation); + } else { + this._scrollable.setScrollPositionNow(update); + } } public getScrollPosition(): IScrollPosition { @@ -593,12 +605,13 @@ export class DomScrollableElement extends ScrollableElement { } function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions { - let result: ScrollableElementResolvedOptions = { + const result: ScrollableElementResolvedOptions = { lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false), className: (typeof opts.className !== 'undefined' ? opts.className : ''), useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true), handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true), flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false), + consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false), alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false), scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false), mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1), diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts index dd4c57a38f..43837bd835 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts @@ -40,6 +40,11 @@ export interface ScrollableElementCreationOptions { * Defaults to false. */ scrollYToX?: boolean; + /** + * Consume all mouse wheel events if a scrollbar is needed (i.e. scrollSize > size). + * Defaults to false. + */ + consumeMouseWheelIfScrollbarIsNeeded?: boolean; /** * Always consume mouse wheel events, even when scrolling is no longer possible. * Defaults to false. @@ -136,6 +141,7 @@ export interface ScrollableElementResolvedOptions { handleMouseWheel: boolean; flipAxes: boolean; scrollYToX: boolean; + consumeMouseWheelIfScrollbarIsNeeded: boolean; alwaysConsumeMouseWheel: boolean; mouseWheelScrollSensitivity: number; fastScrollSensitivity: number; diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts index b63570e485..6d84efb9ac 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts @@ -88,7 +88,7 @@ export class ScrollbarArrow extends Widget { } private _arrowMouseDown(e: IMouseEvent): void { - let scheduleRepeater = () => { + const scheduleRepeater = () => { this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24); }; diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarState.ts b/src/vs/base/browser/ui/scrollbar/scrollbarState.ts index 427d3a8edf..d6b8753bf4 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarState.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarState.ts @@ -85,7 +85,7 @@ export class ScrollbarState { } public setVisibleSize(visibleSize: number): boolean { - let iVisibleSize = Math.round(visibleSize); + const iVisibleSize = Math.round(visibleSize); if (this._visibleSize !== iVisibleSize) { this._visibleSize = iVisibleSize; this._refreshComputedValues(); @@ -95,7 +95,7 @@ export class ScrollbarState { } public setScrollSize(scrollSize: number): boolean { - let iScrollSize = Math.round(scrollSize); + const iScrollSize = Math.round(scrollSize); if (this._scrollSize !== iScrollSize) { this._scrollSize = iScrollSize; this._refreshComputedValues(); @@ -105,7 +105,7 @@ export class ScrollbarState { } public setScrollPosition(scrollPosition: number): boolean { - let iScrollPosition = Math.round(scrollPosition); + const iScrollPosition = Math.round(scrollPosition); if (this._scrollPosition !== iScrollPosition) { this._scrollPosition = iScrollPosition; this._refreshComputedValues(); @@ -198,7 +198,7 @@ export class ScrollbarState { return 0; } - let desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2; + const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2; return Math.round(desiredSliderPosition / this._computedSliderRatio); } @@ -214,7 +214,7 @@ export class ScrollbarState { return 0; } - let correctedOffset = offset - this._arrowSize; // compensate if has arrows + const correctedOffset = offset - this._arrowSize; // compensate if has arrows let desiredScrollPosition = this._scrollPosition; if (correctedOffset < this._computedSliderPosition) { desiredScrollPosition -= this._visibleSize; // page up/left @@ -233,7 +233,7 @@ export class ScrollbarState { return 0; } - let desiredSliderPosition = this._computedSliderPosition + delta; + const desiredSliderPosition = this._computedSliderPosition + delta; return Math.round(desiredSliderPosition / this._computedSliderRatio); } } diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts b/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts index efcbfb47a0..8bbc0e71ff 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts @@ -43,7 +43,7 @@ export class ScrollbarVisibilityController extends Disposable { } public setShouldBeVisible(rawShouldBeVisible: boolean): void { - let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible); + const shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible); if (this._shouldBeVisible !== shouldBeVisible) { this._shouldBeVisible = shouldBeVisible; @@ -105,4 +105,4 @@ export class ScrollbarVisibilityController extends Disposable { this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : '')); } } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts index 469482ee5a..981340fa99 100644 --- a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts @@ -38,8 +38,8 @@ export class VerticalScrollbar extends AbstractScrollbar { }); if (options.verticalHasArrows) { - let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2; - let scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2; + const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2; + const scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2; this._createArrow({ className: 'scra', diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index c6a4779544..262385ab4b 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -73,7 +73,7 @@ export class ToolBar extends Disposable { actionViewItemProvider: this.options.actionViewItemProvider, actionRunner: this.actionRunner, keybindingProvider: this.options.getKeyBinding, - classNames: (options.moreIcon ?? toolBarMoreIcon).classNames, + classNames: CSSIcon.asClassNameArray(options.moreIcon ?? toolBarMoreIcon), anchorAlignmentProvider: this.options.anchorAlignmentProvider, menuAsChild: !!this.options.renderDropdownAsChildElement } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 27c4e6e9f3..41bb922ac2 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1117,9 +1117,7 @@ class TreeNodeListMouseController extends MouseController< expandOnlyOnTwistieClick = !!this.tree.expandOnlyOnTwistieClick; } - const clickedOnFocus = this.tree.getFocus()[0] === node.element; - - if (expandOnlyOnTwistieClick && !onTwistie && e.browserEvent.detail !== 2 && !(clickedOnFocus && !node.collapsed)) { + if (expandOnlyOnTwistieClick && !onTwistie && e.browserEvent.detail !== 2) { return super.onViewPointer(e); } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 5a600cd594..24f5588904 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; -import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, IObjectTreeSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTree'; import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; -import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { timeout, CancelablePromise, createCancelablePromise, Promises } from 'vs/base/common/async'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { Iterable } from 'vs/base/common/iterator'; import { IDragAndDropData } from 'vs/base/browser/dnd'; @@ -279,6 +279,7 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt } export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { } +export interface IAsyncDataTreeUpdateChildrenOptions extends IObjectTreeSetChildrenOptions { } export interface IAsyncDataTreeOptions extends IAsyncDataTreeOptionsUpdate, Pick, Exclude, 'collapseByDefault'>> { readonly collapseByDefault?: { (e: T): boolean; }; @@ -499,11 +500,11 @@ export class AsyncDataTree implements IDisposable } } - async updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false): Promise { - await this._updateChildren(element, recursive, rerender); + async updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, options?: IAsyncDataTreeUpdateChildrenOptions): Promise { + await this._updateChildren(element, recursive, rerender, undefined, options); } - private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext): Promise { + private async _updateChildren(element: TInput | T = this.root.element, recursive = true, rerender = false, viewStateContext?: IAsyncDataTreeViewStateContext, options?: IAsyncDataTreeUpdateChildrenOptions): Promise { if (typeof this.root.element === 'undefined') { throw new TreeError(this.user, 'Tree input not set'); } @@ -514,7 +515,7 @@ export class AsyncDataTree implements IDisposable } const node = this.getDataNode(element); - await this.refreshAndRenderNode(node, recursive, viewStateContext); + await this.refreshAndRenderNode(node, recursive, viewStateContext, options); if (rerender) { try { @@ -704,9 +705,9 @@ export class AsyncDataTree implements IDisposable return node; } - private async refreshAndRenderNode(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext): Promise { + private async refreshAndRenderNode(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext, options?: IAsyncDataTreeUpdateChildrenOptions): Promise { await this.refreshNode(node, recursive, viewStateContext); - this.render(node, viewStateContext); + this.render(node, viewStateContext, options); } private async refreshNode(node: IAsyncDataTreeNode, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext): Promise { @@ -739,7 +740,7 @@ export class AsyncDataTree implements IDisposable const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext); node.stale = false; - await Promise.all(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext))); + await Promises.settled(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext))); } finally { done!(); } @@ -768,8 +769,8 @@ export class AsyncDataTree implements IDisposable const children = await childrenPromise; return this.setChildren(node, children, recursive, viewStateContext); } catch (err) { - if (node !== this.root) { - this.tree.collapse(node === this.root ? null : node); + if (node !== this.root && this.tree.hasElement(node)) { + this.tree.collapse(node); } if (isPromiseCanceledError(err)) { @@ -921,9 +922,18 @@ export class AsyncDataTree implements IDisposable return childrenToRefresh; } - protected render(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): void { + protected render(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext, options?: IAsyncDataTreeUpdateChildrenOptions): void { const children = node.children.map(node => this.asTreeElement(node, viewStateContext)); - this.tree.setChildren(node === this.root ? null : node, children); + const objectTreeOptions: IObjectTreeSetChildrenOptions> | undefined = options && { + ...options, + diffIdentityProvider: options!.diffIdentityProvider && { + getId(node: IAsyncDataTreeNode): { toString(): string; } { + return options!.diffIdentityProvider!.getId(node.element as T); + } + } + }; + + this.tree.setChildren(node === this.root ? null : node, children, objectTreeOptions); if (node !== this.root) { this.tree.setCollapsible(node, node.hasChildren); @@ -980,16 +990,16 @@ export class AsyncDataTree implements IDisposable const expanded: string[] = []; const root = this.tree.getNode(); - const queue = [root]; + const stack = [root]; - while (queue.length > 0) { - const node = queue.shift()!; + while (stack.length > 0) { + const node = stack.pop()!; if (node !== root && node.collapsible && !node.collapsed) { expanded.push(getId(node.element!.element as T)); } - queue.push(...node.children); + stack.push(...node.children); } return { focus, selection, expanded, scrollTop: this.scrollTop }; diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 84b2b92501..0b3d53bba2 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -6,8 +6,9 @@ import { Iterable } from 'vs/base/common/iterator'; import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; -import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; -import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel, IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel'; +import { IIndexTreeModelSpliceOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; // Exported only for test reasons, do not use directly export interface ICompressedTreeElement extends ITreeElement { @@ -108,6 +109,12 @@ interface ICompressedObjectTreeModelOptions extends IObjectTreeM readonly compressionEnabled?: boolean; } +const wrapIdentityProvider = (base: IIdentityProvider): IIdentityProvider> => ({ + getId(node) { + return node.elements.map(e => base.getId(e).toString()).join('\0'); + } +}); + // Exported only for test reasons, do not use directly export class CompressedObjectTreeModel, TFilterData extends NonNullable = void> implements ITreeModel | null, TFilterData, T | null> { @@ -120,6 +127,7 @@ export class CompressedObjectTreeModel, TFilterData e private model: ObjectTreeModel, TFilterData>; private nodes = new Map>(); private enabled: boolean; + private readonly identityProvider?: IIdentityProvider>; get size(): number { return this.nodes.size; } @@ -130,15 +138,21 @@ export class CompressedObjectTreeModel, TFilterData e ) { this.model = new ObjectTreeModel(user, list, options); this.enabled = typeof options.compressionEnabled === 'undefined' ? true : options.compressionEnabled; + this.identityProvider = options.identityProvider; } setChildren( element: T | null, - children: Iterable> = Iterable.empty() + children: Iterable> = Iterable.empty(), + options: IObjectTreeModelSetChildrenOptions, ): void { + // Diffs must be deem, since the compression can affect nested elements. + // @see https://github.com/microsoft/vscode/pull/114237#issuecomment-759425034 + + const diffIdentityProvider = options.diffIdentityProvider && wrapIdentityProvider(options.diffIdentityProvider); if (element === null) { const compressedChildren = Iterable.map(children, this.enabled ? compress : noCompress); - this._setChildren(null, compressedChildren); + this._setChildren(null, compressedChildren, { diffIdentityProvider, diffDepth: Infinity }); return; } @@ -159,7 +173,10 @@ export class CompressedObjectTreeModel, TFilterData e const parentChildren = parent.children .map(child => child === node ? recompressedElement : child); - this._setChildren(parent.element, parentChildren); + this._setChildren(parent.element, parentChildren, { + diffIdentityProvider, + diffDepth: node.depth - parent.depth, + }); } isCompressionEnabled(): boolean { @@ -177,22 +194,29 @@ export class CompressedObjectTreeModel, TFilterData e const rootChildren = root.children as ITreeNode>[]; const decompressedRootChildren = Iterable.map(rootChildren, decompress); const recompressedRootChildren = Iterable.map(decompressedRootChildren, enabled ? compress : noCompress); - this._setChildren(null, recompressedRootChildren); + + // it should be safe to always use deep diff mode here if an identity + // provider is available, since we know the raw nodes are unchanged. + this._setChildren(null, recompressedRootChildren, { + diffIdentityProvider: this.identityProvider, + diffDepth: Infinity, + }); } private _setChildren( node: ICompressedTreeNode | null, - children: Iterable>> + children: Iterable>>, + options: IIndexTreeModelSpliceOptions, TFilterData>, ): void { const insertedElements = new Set(); - const _onDidCreateNode = (node: ITreeNode, TFilterData>) => { + const onDidCreateNode = (node: ITreeNode, TFilterData>) => { for (const element of node.element.elements) { insertedElements.add(element); this.nodes.set(element, node.element); } }; - const _onDidDeleteNode = (node: ITreeNode, TFilterData>) => { + const onDidDeleteNode = (node: ITreeNode, TFilterData>) => { for (const element of node.element.elements) { if (!insertedElements.has(element)) { this.nodes.delete(element); @@ -200,7 +224,7 @@ export class CompressedObjectTreeModel, TFilterData e } }; - this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode); + this.model.setChildren(node, children, { ...options, onDidCreateNode, onDidDeleteNode }); } has(element: T | null): boolean { @@ -363,16 +387,16 @@ function mapList(nodeMapper: CompressedNodeWeakMapper(compressedNodeUnwrapper: CompressedNodeUnwrapper, options: ICompressibleObjectTreeModelOptions): ICompressedObjectTreeModelOptions { return { ...options, - sorter: options.sorter && { - compare(node: ICompressedTreeNode, otherNode: ICompressedTreeNode): number { - return options.sorter!.compare(node.elements[0], otherNode.elements[0]); - } - }, identityProvider: options.identityProvider && { getId(node: ICompressedTreeNode): { toString(): string; } { return options.identityProvider!.getId(compressedNodeUnwrapper(node)); } }, + sorter: options.sorter && { + compare(node: ICompressedTreeNode, otherNode: ICompressedTreeNode): number { + return options.sorter!.compare(node.elements[0], otherNode.elements[0]); + } + }, filter: options.filter && { filter(node: ICompressedTreeNode, parentVisibility: TreeVisibility): TreeFilterResult { return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility); @@ -424,8 +448,12 @@ export class CompressibleObjectTreeModel, TFilterData this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options)); } - setChildren(element: T | null, children: Iterable> = Iterable.empty()): void { - this.model.setChildren(element, children); + setChildren( + element: T | null, + children: Iterable> = Iterable.empty(), + options: IObjectTreeModelSetChildrenOptions = {}, + ): void { + this.model.setChildren(element, children, options); } isCompressionEnabled(): boolean { diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index 3c39bbb95f..6c62c57870 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -47,13 +47,19 @@ export class DataTree extends AbstractTree extends AbstractTree boolean | undefined): { elements: Iterable>, size: number } { diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index ba9b016289..12f900f933 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -3,8 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree'; import { splice, tail2 } from 'vs/base/common/arrays'; +import { LcsDiff } from 'vs/base/common/diff/diff'; import { Emitter, Event, EventBufferer } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { ISpliceable } from 'vs/base/common/sequence'; @@ -41,6 +43,34 @@ export interface IIndexTreeModelOptions { readonly autoExpandSingleChildren?: boolean; } +export interface IIndexTreeModelSpliceOptions { + /** + * If set, child updates will recurse the given number of levels even if + * items in the splice operation are unchanged. `Infinity` is a valid value. + */ + readonly diffDepth?: number; + + /** + * Identity provider used to optimize splice() calls in the IndexTree. If + * this is not present, optimized splicing is not enabled. + * + * Warning: if this is present, calls to `setChildren()` will not replace + * or update nodes if their identity is the same, even if the elements are + * different. For this, you should call `rerender()`. + */ + readonly diffIdentityProvider?: IIdentityProvider; + + /** + * Callback for when a node is created. + */ + onDidCreateNode?: (node: ITreeNode) => void; + + /** + * Callback for when a node is deleted. + */ + onDidDeleteNode?: (node: ITreeNode) => void +} + interface CollapsibleStateUpdate { readonly collapsible: boolean; } @@ -110,18 +140,95 @@ export class IndexTreeModel, TFilterData = voi location: number[], deleteCount: number, toInsert: Iterable> = Iterable.empty(), - onDidCreateNode?: (node: ITreeNode) => void, - onDidDeleteNode?: (node: ITreeNode) => void + options: IIndexTreeModelSpliceOptions = {}, ): void { if (location.length === 0) { throw new TreeError(this.user, 'Invalid tree location'); } + if (options.diffIdentityProvider) { + this.spliceSmart(options.diffIdentityProvider, location, deleteCount, toInsert, options); + } else { + this.spliceSimple(location, deleteCount, toInsert, options); + } + } + + private spliceSmart( + identity: IIdentityProvider, + location: number[], + deleteCount: number, + toInsertIterable: Iterable> = Iterable.empty(), + options: IIndexTreeModelSpliceOptions, + recurseLevels = options.diffDepth ?? 0, + ) { + const { parentNode } = this.getParentNodeWithListIndex(location); + const toInsert = [...toInsertIterable]; + const index = location[location.length - 1]; + const diff = new LcsDiff( + { getElements: () => parentNode.children.map(e => identity.getId(e.element).toString()) }, + { + getElements: () => [ + ...parentNode.children.slice(0, index), + ...toInsert, + ...parentNode.children.slice(index + deleteCount), + ].map(e => identity.getId(e.element).toString()) + }, + ).ComputeDiff(false); + + // if we were given a 'best effort' diff, use default behavior + if (diff.quitEarly) { + return this.spliceSimple(location, deleteCount, toInsert, options); + } + + const locationPrefix = location.slice(0, -1); + const recurseSplice = (fromOriginal: number, fromModified: number, count: number) => { + if (recurseLevels > 0) { + for (let i = 0; i < count; i++) { + fromOriginal--; + fromModified--; + this.spliceSmart( + identity, + [...locationPrefix, fromOriginal, 0], + Number.MAX_SAFE_INTEGER, + toInsert[fromModified].children, + options, + recurseLevels - 1, + ); + } + } + }; + + let lastStartO = Math.min(parentNode.children.length, index + deleteCount); + let lastStartM = toInsert.length; + for (const change of diff.changes.sort((a, b) => b.originalStart - a.originalStart)) { + recurseSplice(lastStartO, lastStartM, lastStartO - (change.originalStart + change.originalLength)); + lastStartO = change.originalStart; + lastStartM = change.modifiedStart - index; + + this.spliceSimple( + [...locationPrefix, lastStartO], + change.originalLength, + Iterable.slice(toInsert, lastStartM, lastStartM + change.modifiedLength), + options, + ); + } + + // at this point, startO === startM === count since any remaining prefix should match + recurseSplice(lastStartO, lastStartM, lastStartO); + } + + private spliceSimple( + location: number[], + deleteCount: number, + toInsert: Iterable> = Iterable.empty(), + { onDidCreateNode, onDidDeleteNode }: IIndexTreeModelSpliceOptions, + ) { const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location); const treeListElementsToInsert: ITreeNode[] = []; const nodesToInsertIterator = Iterable.map(toInsert, el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode)); const lastIndex = location[location.length - 1]; + const lastHadChildren = parentNode.children.length > 0; // figure out what's the visible child start index right before the // splice point @@ -190,6 +297,11 @@ export class IndexTreeModel, TFilterData = voi deletedNodes.forEach(visit); } + const currentlyHasChildren = parentNode.children.length > 0; + if (lastHadChildren !== currentlyHasChildren) { + this.setCollapsible(location.slice(0, -1), currentlyHasChildren); + } + this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes }); let node: IIndexTreeNode | undefined = parentNode; diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index ba0c64d82d..327f52bf58 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -7,7 +7,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; -import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { memoize } from 'vs/base/common/decorators'; @@ -17,6 +17,25 @@ export interface IObjectTreeOptions extends IAbstractTree readonly sorter?: ITreeSorter; } +export interface IObjectTreeSetChildrenOptions { + + /** + * If set, child updates will recurse the given number of levels even if + * items in the splice operation are unchanged. `Infinity` is a valid value. + */ + readonly diffDepth?: number; + + /** + * Identity provider used to optimize splice() calls in the IndexTree. If + * this is not present, optimized splicing is not enabled. + * + * Warning: if this is present, calls to `setChildren()` will not replace + * or update nodes if their identity is the same, even if the elements are + * different. For this, you should call `rerender()`. + */ + readonly diffIdentityProvider?: IIdentityProvider; +} + export class ObjectTree, TFilterData = void> extends AbstractTree { protected model!: IObjectTreeModel; @@ -33,8 +52,8 @@ export class ObjectTree, TFilterData = void> extends super(user, container, delegate, renderers, options as IObjectTreeOptions); } - setChildren(element: T | null, children: Iterable> = Iterable.empty()): void { - this.model.setChildren(element, children); + setChildren(element: T | null, children: Iterable> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions): void { + this.model.setChildren(element, children, options); } rerender(element?: T): void { @@ -189,8 +208,8 @@ export class CompressibleObjectTree, TFilterData = vo super(user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options)); } - setChildren(element: T | null, children: Iterable> = Iterable.empty()): void { - this.model.setChildren(element, children); + setChildren(element: T | null, children: Iterable> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions): void { + this.model.setChildren(element, children, options); } protected createModel(user: string, view: IList>, options: ICompressibleObjectTreeOptions): ITreeModel { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index a3b48e8185..90e9f922fb 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Iterable } from 'vs/base/common/iterator'; -import { IndexTreeModel, IIndexTreeModelOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IndexTreeModel, IIndexTreeModelOptions, IList, IIndexTreeModelSpliceOptions } from 'vs/base/browser/ui/tree/indexTreeModel'; import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -13,11 +13,14 @@ import { mergeSort } from 'vs/base/common/arrays'; export type ITreeNodeCallback = (node: ITreeNode) => void; export interface IObjectTreeModel, TFilterData extends NonNullable = void> extends ITreeModel { - setChildren(element: T | null, children: Iterable> | undefined): void; + setChildren(element: T | null, children: Iterable> | undefined, options?: IObjectTreeModelSetChildrenOptions): void; resort(element?: T | null, recursive?: boolean): void; updateElementHeight(element: T, height: number): void; } +export interface IObjectTreeModelSetChildrenOptions extends IIndexTreeModelSpliceOptions { +} + export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { readonly sorter?: ITreeSorter; readonly identityProvider?: IIdentityProvider; @@ -63,23 +66,21 @@ export class ObjectTreeModel, TFilterData extends Non setChildren( element: T | null, children: Iterable> = Iterable.empty(), - onDidCreateNode?: ITreeNodeCallback, - onDidDeleteNode?: ITreeNodeCallback + options: IObjectTreeModelSetChildrenOptions = {}, ): void { const location = this.getElementLocation(element); - this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode); + this._setChildren(location, this.preserveCollapseState(children), options); } private _setChildren( location: number[], children: Iterable> = Iterable.empty(), - onDidCreateNode?: ITreeNodeCallback, - onDidDeleteNode?: ITreeNodeCallback + options: IObjectTreeModelSetChildrenOptions, ): void { const insertedElements = new Set(); const insertedElementIds = new Set(); - const _onDidCreateNode = (node: ITreeNode) => { + const onDidCreateNode = (node: ITreeNode) => { if (node.element === null) { return; } @@ -95,12 +96,10 @@ export class ObjectTreeModel, TFilterData extends Non this.nodesByIdentity.set(id, tnode); } - if (onDidCreateNode) { - onDidCreateNode(tnode); - } + options.onDidCreateNode?.(tnode); }; - const _onDidDeleteNode = (node: ITreeNode) => { + const onDidDeleteNode = (node: ITreeNode) => { if (node.element === null) { return; } @@ -118,17 +117,14 @@ export class ObjectTreeModel, TFilterData extends Non } } - if (onDidDeleteNode) { - onDidDeleteNode(tnode); - } + options.onDidDeleteNode?.(tnode); }; this.model.splice( [...location, 0], Number.MAX_VALUE, children, - _onDidCreateNode, - _onDidDeleteNode + { ...options, onDidCreateNode, onDidDeleteNode } ); } @@ -182,7 +178,7 @@ export class ObjectTreeModel, TFilterData extends Non const location = this.getElementLocation(element); const node = this.model.getNode(location); - this._setChildren(location, this.resortChildren(node, recursive)); + this._setChildren(location, this.resortChildren(node, recursive), {}); } private resortChildren(node: ITreeNode, recursive: boolean, first = true): Iterable> { diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index eff13ee02a..dd1e481903 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -14,8 +14,8 @@ export interface ITelemetryData { } export type WorkbenchActionExecutedClassification = { - id: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - from: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + id: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + from: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; }; export type WorkbenchActionExecutedEvent = { @@ -65,7 +65,7 @@ export interface IActionChangeEvent { export class Action extends Disposable implements IAction { protected _onDidChange = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + readonly onDidChange = this._onDidChange.event; protected readonly _id: string; protected _label: string; @@ -198,10 +198,10 @@ export interface IRunEvent { export class ActionRunner extends Disposable implements IActionRunner { private _onBeforeRun = this._register(new Emitter()); - readonly onBeforeRun: Event = this._onBeforeRun.event; + readonly onBeforeRun = this._onBeforeRun.event; private _onDidRun = this._register(new Emitter()); - readonly onDidRun: Event = this._onDidRun.event; + readonly onDidRun = this._onDidRun.event; async run(action: IAction, context?: any): Promise { if (!action.enabled) { @@ -265,14 +265,43 @@ export class ActionWithMenuAction extends Action { } } -export class SubmenuAction extends Action { +export class SubmenuAction implements IAction { - get actions(): IAction[] { + readonly id: string; + readonly label: string; + readonly class: string | undefined; + readonly tooltip: string = ''; + readonly enabled: boolean = true; + readonly checked: boolean = false; + + private readonly _actions: readonly IAction[]; + + constructor(id: string, label: string, actions: readonly IAction[], cssClass?: string) { + this.id = id; + this.label = label; + this.class = cssClass; + this._actions = actions; + } + + dispose(): void { + // there is NOTHING to dispose and the SubmenuAction should + // never have anything to dispose as it is a convenience type + // to bridge into the rendering world. + } + + get actions(): readonly IAction[] { return this._actions; } - constructor(id: string, label: string, private _actions: IAction[], cssClass?: string) { - super(id, label, cssClass, !!_actions?.length); + async run(): Promise { } + + // {{SQL CARBON EDIT}} + get expanded(): boolean { + return false; + } + set expanded(value: boolean) { + } + protected _setExpanded(value: boolean): void { } } @@ -282,3 +311,18 @@ export class EmptySubmenuAction extends Action { super(EmptySubmenuAction.ID, nls.localize('submenu.empty', '(empty)'), undefined, false); } } + +export function toAction(props: { id: string, label: string, enabled?: boolean, checked?: boolean, run: Function; }): IAction { + return { + id: props.id, + label: props.label, + class: undefined, + // {{SQL CARBON EDIT}} - add expanded type + expanded: false, + enabled: props.enabled ?? true, + checked: props.checked ?? false, + run: async () => props.run(), + tooltip: props.label, + dispose: () => { } + }; +} diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index f179bdbb49..65dd360cdc 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import * as errors from 'vs/base/common/errors'; -import { Emitter, Event } from 'vs/base/common/event'; +import { canceled, onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event, Listener } from 'vs/base/common/event'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; import { URI } from 'vs/base/common/uri'; export function isThenable(obj: any): obj is Promise { @@ -23,7 +24,7 @@ export function createCancelablePromise(callback: (token: CancellationToken) const thenable = callback(source.token); const promise = new Promise((resolve, reject) => { source.token.onCancellationRequested(() => { - reject(errors.canceled()); + reject(canceled()); }); Promise.resolve(thenable).then(value => { source.dispose(); @@ -152,13 +153,13 @@ export class Throttler { return result; }; - this.queuedPromise = new Promise(c => { - this.activePromise!.then(onComplete, onComplete).then(c); + this.queuedPromise = new Promise(resolve => { + this.activePromise!.then(onComplete, onComplete).then(resolve); }); } - return new Promise((c, e) => { - this.queuedPromise!.then(c, e); + return new Promise((resolve, reject) => { + this.queuedPromise!.then(resolve, reject); }); } @@ -181,7 +182,7 @@ export class Sequencer { private current: Promise = Promise.resolve(null); queue(promiseTask: ITask>): Promise { - return this.current = this.current.then(() => promiseTask()); + return this.current = this.current.then(() => promiseTask(), () => promiseTask()); } } @@ -205,7 +206,7 @@ export class SequencerByKey { } /** - * A helper to delay execution of a task that is being requested often. + * A helper to delay (debounce) execution of a task that is being requested often. * * Following the throttler, now imagine the mail man wants to optimize the number of * trips proactively. The trip itself can be long, so he decides not to make the trip @@ -248,9 +249,9 @@ export class Delayer implements IDisposable { this.cancelTimeout(); if (!this.completionPromise) { - this.completionPromise = new Promise((c, e) => { - this.doResolve = c; - this.doReject = e; + this.completionPromise = new Promise((resolve, reject) => { + this.doResolve = resolve; + this.doReject = reject; }).then(() => { this.completionPromise = null; this.doResolve = null; @@ -282,7 +283,7 @@ export class Delayer implements IDisposable { if (this.completionPromise) { if (this.doReject) { - this.doReject(errors.canceled()); + this.doReject(canceled()); } this.completionPromise = null; } @@ -377,7 +378,7 @@ export function timeout(millis: number, token?: CancellationToken): CancelablePr const handle = setTimeout(resolve, millis); token.onCancellationRequested(() => { clearTimeout(handle); - reject(errors.canceled()); + reject(canceled()); }); }); } @@ -1013,3 +1014,206 @@ export class IntervalCounter { } //#endregion + +//#region + +export type ValueCallback = (value: T | Promise) => void; + +/** + * Creates a promise whose resolution or rejection can be controlled imperatively. + */ +export class DeferredPromise { + + private completeCallback!: ValueCallback; + private errorCallback!: (err: any) => void; + private rejected = false; + private resolved = false; + + public get isRejected() { + return this.rejected; + } + + public get isResolved() { + return this.resolved; + } + + public get isSettled() { + return this.rejected || this.resolved; + } + + public p: Promise; + + constructor() { + this.p = new Promise((c, e) => { + this.completeCallback = c; + this.errorCallback = e; + }); + } + + public complete(value: T) { + return new Promise(resolve => { + this.completeCallback(value); + this.resolved = true; + resolve(); + }); + } + + public error(err: any) { + return new Promise(resolve => { + this.errorCallback(err); + this.rejected = true; + resolve(); + }); + } + + public cancel() { + new Promise(resolve => { + this.errorCallback(canceled()); + this.rejected = true; + resolve(); + }); + } +} + +//#endregion + +//#region + +export interface IWaitUntil { + waitUntil(thenable: Promise): void; +} + +export class AsyncEmitter extends Emitter { + + private _asyncDeliveryQueue?: LinkedList<[Listener, Omit]>; + + async fireAsync(data: Omit, token: CancellationToken, promiseJoin?: (p: Promise, listener: Function) => Promise): Promise { + if (!this._listeners) { + return; + } + + if (!this._asyncDeliveryQueue) { + this._asyncDeliveryQueue = new LinkedList(); + } + + for (const listener of this._listeners) { + this._asyncDeliveryQueue.push([listener, data]); + } + + while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) { + + const [listener, data] = this._asyncDeliveryQueue.shift()!; + const thenables: Promise[] = []; + + const event = { + ...data, + waitUntil: (p: Promise): void => { + if (Object.isFrozen(thenables)) { + throw new Error('waitUntil can NOT be called asynchronous'); + } + if (promiseJoin) { + p = promiseJoin(p, typeof listener === 'function' ? listener : listener[0]); + } + thenables.push(p); + } + }; + + try { + if (typeof listener === 'function') { + listener.call(undefined, event); + } else { + listener[0].call(listener[1], event); + } + } catch (e) { + onUnexpectedError(e); + continue; + } + + // freeze thenables-collection to enforce sync-calls to + // wait until and then wait for all thenables to resolve + Object.freeze(thenables); + await Promises.settled(thenables).catch(e => onUnexpectedError(e)); + } + } +} + +//#endregion + +//#region Promises + +export namespace Promises { + + export interface IResolvedPromise { + status: 'fulfilled'; + value: T; + } + + export interface IRejectedPromise { + status: 'rejected'; + reason: Error; + } + + /** + * Interface of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled + */ + interface PromiseWithAllSettled { + allSettled(promises: Promise[]): Promise | IRejectedPromise>>; + } + + /** + * A polyfill of `Promise.allSettled`: returns after all promises have + * resolved or rejected and provides access to each result or error + * in the order of the original passed in promises array. + * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled + */ + export async function allSettled(promises: Promise[]): Promise | IRejectedPromise>> { + if (typeof (Promise as unknown as PromiseWithAllSettled).allSettled === 'function') { + return allSettledNative(promises); // in some environments we can benefit from native implementation + } + + return allSettledShim(promises); + } + + async function allSettledNative(promises: Promise[]): Promise | IRejectedPromise>> { + return (Promise as unknown as PromiseWithAllSettled).allSettled(promises); + } + + async function allSettledShim(promises: Promise[]): Promise | IRejectedPromise>> { + return Promise.all(promises.map(promise => (promise.then(value => { + const fulfilled: IResolvedPromise = { status: 'fulfilled', value }; + + return fulfilled; + }, error => { + const rejected: IRejectedPromise = { status: 'rejected', reason: error }; + + return rejected; + })))); + } + + /** + * A drop-in replacement for `Promise.all` with the only difference + * that the method awaits every promise to either fulfill or reject. + * + * Similar to `Promise.all`, only the first error will be returned + * if any. + */ + export async function settled(promises: Promise[]): Promise { + let firstError: Error | undefined = undefined; + + const result = await Promise.all(promises.map(promise => promise.then(value => value, error => { + if (!firstError) { + firstError = error; + } + + return undefined; // do not rethrow so that other promises can settle + }))); + + if (firstError) { + throw firstError; + } + + return result as unknown as T[]; // cast is needed and protected by the `throw` above + } +} + +//#endregion diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index b648dc2618..7131f786e4 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -34,8 +34,9 @@ export class VSBuffer { return new VSBuffer(actual); } - static fromString(source: string): VSBuffer { - if (hasBuffer) { + static fromString(source: string, options?: { dontUseNodeBuffer?: boolean; }): VSBuffer { + const dontUseNodeBuffer = options?.dontUseNodeBuffer || false; + if (!dontUseNodeBuffer && hasBuffer) { return new VSBuffer(Buffer.from(source)); } else if (hasTextEncoder) { if (!textEncoder) { diff --git a/src/vs/base/common/buildunit.json b/src/vs/base/common/buildunit.json deleted file mode 100644 index 50e3d75067..0000000000 --- a/src/vs/base/common/buildunit.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "vs/base", - "dependencies": [ - { - "name": "vs", - "internal": false - } - ], - "libs": [ - "lib.core.d.ts" - ], - "sources": [ - "**/*.ts" - ], - "declares": [] -} diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 3d3817103d..2a835cc81d 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -61,8 +61,57 @@ export class Codicon implements CSSIcon { public get cssSelector() { return '.codicon.codicon-' + this.id; } } +export function getClassNamesArray(id: string, modifier?: string) { + const classNames = ['codicon', 'codicon-' + id]; + if (modifier) { + classNames.push('codicon-modifier-' + modifier); + } + return classNames; +} + export interface CSSIcon { - readonly classNames: string; + readonly id: string; +} + + +export namespace CSSIcon { + export const iconNameExpression = '[A-Za-z0-9\\-]+'; + export const iconModifierExpression = '~[A-Za-z]+'; + + const cssIconIdRegex = new RegExp(`^(${iconNameExpression})(${iconModifierExpression})?$`); + + export function asClassNameArray(icon: CSSIcon): string[] { + if (icon instanceof Codicon) { + return ['codicon', 'codicon-' + icon.id]; + } + const match = cssIconIdRegex.exec(icon.id); + if (!match) { + return asClassNameArray(Codicon.error); + } + let [, id, modifier] = match; + + // {{SQL CARBON EDIT}} Modifying method to not add 'codicon' in front of sql carbon icons. + let sqlCarbonIcons = ['book', 'dataExplorer']; + if (sqlCarbonIcons.includes(id)) { + return [id]; + // {{SQL CARBON EDIT}} End of edit + } else { + const classNames = ['codicon', 'codicon-' + id]; + if (modifier) { + classNames.push('codicon-modifier-' + modifier.substr(1)); + } + return classNames; + } + + } + + export function asClassName(icon: CSSIcon): string { + return asClassNameArray(icon).join(' '); + } + + export function asCSSSelector(icon: CSSIcon): string { + return '.' + asClassNameArray(icon).join('.'); + } } @@ -498,6 +547,14 @@ export namespace Codicon { export const passFilled = new Codicon('pass-filled', { character: '\\ebb3' }); export const circleLargeFilled = new Codicon('circle-large-filled', { character: '\\ebb4' }); export const circleLargeOutline = new Codicon('circle-large-outline', { character: '\\ebb5' }); + export const combine = new Codicon('combine', { character: '\\ebb6' }); + export const gather = new Codicon('gather', { character: '\\ebb6' }); + export const table = new Codicon('table', { character: '\\ebb7' }); + export const variableGroup = new Codicon('variable-group', { character: '\\ebb8' }); + export const typeHierarchy = new Codicon('type-hierarchy', { character: '\\ebb9' }); + export const typeHierarchySub = new Codicon('type-hierarchy-sub', { character: '\\ebba' }); + export const typeHierarchySuper = new Codicon('type-hierarchy-super', { character: '\\ebbb' }); + export const gitPullRequestCreate = new Codicon('git-pull-request-create', { character: '\\ebbc' }); export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition, localize('dropDownButton', 'Icon for drop down buttons.')); } diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index ba5434f2c1..98a831f909 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -142,6 +142,15 @@ export function isPromiseCanceledError(error: any): boolean { return error instanceof Error && error.name === canceledName && error.message === canceledName; } +// !!!IMPORTANT!!! +// Do NOT change this class because it is also used as an API-type. +export class CancellationError extends Error { + constructor() { + super(canceledName); + this.name = this.message; + } +} + /** * Returns an error that signals cancellation. */ diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index d7638a5549..2564f6603b 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { once as onceFn } from 'vs/base/common/functional'; import { Disposable, IDisposable, toDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { StopWatch } from 'vs/base/common/stopwatch'; /** * To an event a function with one or zero parameters @@ -374,11 +374,11 @@ export namespace Event { } export function toPromise(event: Event): Promise { - return new Promise(c => once(event)(c)); + return new Promise(resolve => once(event)(resolve)); } } -type Listener = [(e: T) => void, any] | ((e: T) => void); +export type Listener = [(e: T) => void, any] | ((e: T) => void); export interface EmitterOptions { onFirstListenerAdd?: Function; @@ -386,6 +386,41 @@ export interface EmitterOptions { onListenerDidAdd?: Function; onLastListenerRemove?: Function; leakWarningThreshold?: number; + + /** ONLY enable this during development */ + _profName?: string +} + + +class EventProfiling { + + private static _idPool = 0; + + private _name: string; + private _stopWatch?: StopWatch; + private _listenerCount: number = 0; + private _invocationCount = 0; + private _elapsedOverall = 0; + + constructor(name: string) { + this._name = `${name}_${EventProfiling._idPool++}`; + } + + start(listenerCount: number): void { + this._stopWatch = new StopWatch(true); + this._listenerCount = listenerCount; + } + + stop(): void { + if (this._stopWatch) { + const elapsed = this._stopWatch.elapsed(); + this._elapsedOverall += elapsed; + this._invocationCount += 1; + + console.info(`did FIRE ${this._name}: elapsed_ms: ${elapsed.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`); + this._stopWatch = undefined; + } + } } let _globalLeakWarningThreshold = -1; @@ -487,6 +522,7 @@ export class Emitter { private readonly _options?: EmitterOptions; private readonly _leakageMon?: LeakageMonitor; + private readonly _perfMon?: EventProfiling; private _disposed: boolean = false; private _event?: Event; private _deliveryQueue?: LinkedList<[Listener, T]>; @@ -494,9 +530,8 @@ export class Emitter { constructor(options?: EmitterOptions) { this._options = options; - this._leakageMon = _globalLeakWarningThreshold > 0 - ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) - : undefined; + this._leakageMon = _globalLeakWarningThreshold > 0 ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) : undefined; + this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined; } /** @@ -527,10 +562,7 @@ export class Emitter { } // check and record this emitter for potential leakage - let removeMonitor: (() => void) | undefined; - if (this._leakageMon) { - removeMonitor = this._leakageMon.check(this._listeners.size); - } + const removeMonitor = this._leakageMon?.check(this._listeners.size); let result: IDisposable; result = { @@ -580,6 +612,9 @@ export class Emitter { this._deliveryQueue.push([listener, event]); } + // start/stop performance insight collection + this._perfMon?.start(this._deliveryQueue.size); + while (this._deliveryQueue.size > 0) { const [listener, event] = this._deliveryQueue.shift()!; try { @@ -592,19 +627,15 @@ export class Emitter { onUnexpectedError(e); } } + + this._perfMon?.stop(); } } dispose() { - if (this._listeners) { - this._listeners.clear(); - } - if (this._deliveryQueue) { - this._deliveryQueue.clear(); - } - if (this._leakageMon) { - this._leakageMon.dispose(); - } + this._listeners?.clear(); + this._deliveryQueue?.clear(); + this._leakageMon?.dispose(); this._disposed = true; } } @@ -617,7 +648,7 @@ export class PauseableEmitter extends Emitter { constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) { super(options); - this._mergeFn = options && options.merge; + this._mergeFn = options?.merge; } pause(): void { @@ -654,64 +685,6 @@ export class PauseableEmitter extends Emitter { } } -export interface IWaitUntil { - waitUntil(thenable: Promise): void; -} - -export class AsyncEmitter extends Emitter { - - private _asyncDeliveryQueue?: LinkedList<[Listener, Omit]>; - - async fireAsync(data: Omit, token: CancellationToken, promiseJoin?: (p: Promise, listener: Function) => Promise): Promise { - if (!this._listeners) { - return; - } - - if (!this._asyncDeliveryQueue) { - this._asyncDeliveryQueue = new LinkedList(); - } - - for (const listener of this._listeners) { - this._asyncDeliveryQueue.push([listener, data]); - } - - while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) { - - const [listener, data] = this._asyncDeliveryQueue.shift()!; - const thenables: Promise[] = []; - - const event = { - ...data, - waitUntil: (p: Promise): void => { - if (Object.isFrozen(thenables)) { - throw new Error('waitUntil can NOT be called asynchronous'); - } - if (promiseJoin) { - p = promiseJoin(p, typeof listener === 'function' ? listener : listener[0]); - } - thenables.push(p); - } - }; - - try { - if (typeof listener === 'function') { - listener.call(undefined, event); - } else { - listener[0].call(listener[1], event); - } - } catch (e) { - onUnexpectedError(e); - continue; - } - - // freeze thenables-collection to enforce sync-calls to - // wait until and then wait for all thenables to resolve - Object.freeze(thenables); - await Promise.all(thenables).catch(e => onUnexpectedError(e)); - } - } -} - export class EventMultiplexer implements IDisposable { private readonly emitter: Emitter; diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index 3d5c6c8ac7..ec73edbfc7 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -277,14 +277,25 @@ export function isRootOrDriveLetter(path: string): boolean { return false; } - return isWindowsDriveLetter(pathNormalized.charCodeAt(0)) - && pathNormalized.charCodeAt(1) === CharCode.Colon - && (path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash); + return hasDriveLetter(pathNormalized) && + (path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash); } return pathNormalized === posix.sep; } +export function hasDriveLetter(path: string): boolean { + if (isWindows) { + return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon; + } + + return false; +} + +export function getDriveLetter(path: string): string | undefined { + return hasDriveLetter(path) ? path[0] : undefined; +} + export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number { if (candidate.length > path.length) { return -1; diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index 6e89b1b1f2..d3c25ef265 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -370,24 +370,23 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe if (result) { return result; } - let matches = 0; + let matches: number[] = []; let score = 0; let idx = _wordPos; for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) { const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx); if (wordPos >= 0) { score += 1; - matches += 2 ** wordPos; + matches.unshift(wordPos); idx = wordPos + 1; - - } else if (matches !== 0) { + } else if (matches.length > 0) { // once we have started matching things // we need to match the remaining pattern // characters break; } } - return [score, matches, _wordPos]; + return [score, _wordPos, ...matches]; } //#region --- fuzzyScore --- @@ -396,19 +395,15 @@ export function createMatches(score: undefined | FuzzyScore): IMatch[] { if (typeof score === 'undefined') { return []; } - - const matches = score[1].toString(2); - const wordStart = score[2]; const res: IMatch[] = []; - - for (let pos = wordStart; pos < _maxLen; pos++) { - if (matches[matches.length - (pos + 1)] === '1') { - const last = res[res.length - 1]; - if (last && last.end === pos) { - last.end = pos + 1; - } else { - res.push({ start: pos, end: pos + 1 }); - } + const wordPos = score[1]; + for (let i = score.length - 1; i > 1; i--) { + const pos = score[i] + wordPos; + const last = res[res.length - 1]; + if (last && last.end === pos) { + last.end = pos + 1; + } else { + res.push({ start: pos, end: pos + 1 }); } } return res; @@ -418,20 +413,28 @@ const _maxLen = 128; function initTable() { const table: number[][] = []; - const row: number[] = [0]; - for (let i = 1; i <= _maxLen; i++) { - row.push(-i); + const row: number[] = []; + for (let i = 0; i <= _maxLen; i++) { + row[i] = 0; } for (let i = 0; i <= _maxLen; i++) { - const thisRow = row.slice(0); - thisRow[0] = -i; - table.push(thisRow); + table.push(row.slice(0)); } return table; } +function initArr(maxLen: number) { + const row: number[] = []; + for (let i = 0; i <= maxLen; i++) { + row[i] = 0; + } + return row; +} + +const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position +const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position +const _diag = initTable(); // the length of a contiguous diagonal match const _table = initTable(); -const _scores = initTable(); const _arrows = initTable(); const _debug = false; @@ -460,14 +463,14 @@ function printTables(pattern: string, patternStart: number, word: string, wordSt word = word.substr(wordStart); console.log(printTable(_table, pattern, pattern.length, word, word.length)); console.log(printTable(_arrows, pattern, pattern.length, word, word.length)); - console.log(printTable(_scores, pattern, pattern.length, word, word.length)); + console.log(printTable(_diag, pattern, pattern.length, word, word.length)); } function isSeparatorAtPos(value: string, index: number): boolean { if (index < 0 || index >= value.length) { return false; } - const code = value.charCodeAt(index); + const code = value.codePointAt(index); switch (code) { case CharCode.Underline: case CharCode.Dash: @@ -479,8 +482,16 @@ function isSeparatorAtPos(value: string, index: number): boolean { case CharCode.DoubleQuote: case CharCode.Colon: case CharCode.DollarSign: + case CharCode.LessThan: + case CharCode.OpenParen: + case CharCode.OpenSquareBracket: return true; + case undefined: + return false; default: + if (strings.isEmojiImprecise(code)) { + return true; + } return false; } } @@ -503,9 +514,13 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean { return word[pos] !== wordLow[pos]; } -export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number): boolean { +export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number, fillMinWordPosArr = false): boolean { while (patternPos < patternLen && wordPos < wordLen) { if (patternLow[patternPos] === wordLow[wordPos]) { + if (fillMinWordPosArr) { + // Remember the min word position for each pattern position + _minWordMatchPos[patternPos] = wordPos; + } patternPos += 1; } wordPos += 1; @@ -513,21 +528,24 @@ export function isPatternInWord(patternLow: string, patternPos: number, patternL return patternPos === patternLen; // pattern must be exhausted } -const enum Arrow { Top = 0b1, Diag = 0b10, Left = 0b100 } +const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 } /** - * A tuple of three values. + * An array representating a fuzzy match. + * * 0. the score - * 1. the matches encoded as bitmask (2^53) - * 2. the offset at which matching started + * 1. the offset at which matching started + * 2. `` + * 3. `` + * 4. `` etc */ -export type FuzzyScore = [number, number, number]; +export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number]; export namespace FuzzyScore { /** * No matches and value `-100` */ - export const Default: [-100, 0, 0] = <[-100, 0, 0]>Object.freeze([-100, 0, 0]); + export const Default: FuzzyScore = ([-100, 0]); export function isDefault(score?: FuzzyScore): score is [-100, 0, 0] { return !score || (score[0] === -100 && score[1] === 0 && score[2] === 0); @@ -550,58 +568,71 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu // Run a simple check if the characters of pattern occur // (in order) at all in word. If that isn't the case we // stop because no match will be possible - if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen)) { + if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen, true)) { return undefined; } + // Find the max matching word position for each pattern position + // NOTE: the min matching word position was filled in above, in the `isPatternInWord` call + _fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow); + let row: number = 1; let column: number = 1; let patternPos = patternStart; let wordPos = wordStart; - let hasStrongFirstMatch = false; + const hasStrongFirstMatch = [false]; // There will be a match, fill in tables for (row = 1, patternPos = patternStart; patternPos < patternLen; row++, patternPos++) { - for (column = 1, wordPos = wordStart; wordPos < wordLen; column++, wordPos++) { + // Reduce search space to possible matching word positions and to possible access from next row + const minWordMatchPos = _minWordMatchPos[patternPos]; + const maxWordMatchPos = _maxWordMatchPos[patternPos]; + const nextMaxWordMatchPos = (patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen); - const score = _doScore(pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos); + for (column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos; wordPos < nextMaxWordMatchPos; column++, wordPos++) { - if (patternPos === patternStart && score > 1) { - hasStrongFirstMatch = true; + let score = Number.MIN_SAFE_INTEGER; + let canComeDiag = false; + + if (wordPos <= maxWordMatchPos) { + score = _doScore( + pattern, patternLow, patternPos, patternStart, + word, wordLow, wordPos, wordLen, wordStart, + _diag[row - 1][column - 1] === 0, + hasStrongFirstMatch + ); } - _scores[row][column] = score; + let diagScore = 0; + if (score !== Number.MAX_SAFE_INTEGER) { + canComeDiag = true; + diagScore = score + _table[row - 1][column - 1]; + } - const diag = _table[row - 1][column - 1] + (score > 1 ? 1 : score); - const top = _table[row - 1][column] + -1; - const left = _table[row][column - 1] + -1; + const canComeLeft = wordPos > minWordMatchPos; + const leftScore = canComeLeft ? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0) : 0; // penalty for a gap start - if (left >= top) { - // left or diag - if (left > diag) { - _table[row][column] = left; - _arrows[row][column] = Arrow.Left; - } else if (left === diag) { - _table[row][column] = left; - _arrows[row][column] = Arrow.Left | Arrow.Diag; - } else { - _table[row][column] = diag; - _arrows[row][column] = Arrow.Diag; - } + const canComeLeftLeft = wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0; + const leftLeftScore = canComeLeftLeft ? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0) : 0; // penalty for a gap start + + if (canComeLeftLeft && (!canComeLeft || leftLeftScore >= leftScore) && (!canComeDiag || leftLeftScore >= diagScore)) { + // always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word + _table[row][column] = leftLeftScore; + _arrows[row][column] = Arrow.LeftLeft; + _diag[row][column] = 0; + } else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) { + // always prefer choosing left since that means a match is earlier in the word + _table[row][column] = leftScore; + _arrows[row][column] = Arrow.Left; + _diag[row][column] = 0; + } else if (canComeDiag) { + _table[row][column] = diagScore; + _arrows[row][column] = Arrow.Diag; + _diag[row][column] = _diag[row - 1][column - 1] + 1; } else { - // top or diag - if (top > diag) { - _table[row][column] = top; - _arrows[row][column] = Arrow.Top; - } else if (top === diag) { - _table[row][column] = top; - _arrows[row][column] = Arrow.Top | Arrow.Diag; - } else { - _table[row][column] = diag; - _arrows[row][column] = Arrow.Diag; - } + throw new Error(`not possible`); } } } @@ -610,144 +641,152 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu printTables(pattern, patternStart, word, wordStart); } - if (!hasStrongFirstMatch && !firstMatchCanBeWeak) { + if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) { return undefined; } - _matchesCount = 0; - _topScore = -100; - _wordStart = wordStart; - _firstMatchCanBeWeak = firstMatchCanBeWeak; + row--; + column--; - _findAllMatches2(row - 1, column - 1, patternLen === wordLen ? 1 : 0, 0, false); - if (_matchesCount === 0) { - return undefined; + const result: FuzzyScore = [_table[row][column], wordStart]; + + let backwardsDiagLength = 0; + let maxMatchColumn = 0; + + while (row >= 1) { + // Find the column where we go diagonally up + let diagColumn = column; + do { + const arrow = _arrows[row][diagColumn]; + if (arrow === Arrow.LeftLeft) { + diagColumn = diagColumn - 2; + } else if (arrow === Arrow.Left) { + diagColumn = diagColumn - 1; + } else { + // found the diagonal + break; + } + } while (diagColumn >= 1); + + // Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match + if ( + backwardsDiagLength > 1 // only if we would have a contiguous match of 3 characters + && patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] // only if we can do a contiguous match diagonally + && !isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) // only if the forwards chose diagonal is not an uppercase + && backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match + ) { + diagColumn = column; + } + + if (diagColumn === column) { + // this is a contiguous match + backwardsDiagLength++; + } else { + backwardsDiagLength = 1; + } + + if (!maxMatchColumn) { + // remember the last matched column + maxMatchColumn = diagColumn; + } + + row--; + column = diagColumn - 1; + result.push(column); } - return [_topScore, _topMatch2, wordStart]; + if (wordLen === patternLen) { + // the word matches the pattern with all characters! + // giving the score a total match boost (to come up ahead other words) + result[0] += 2; + } + + // Add 1 penalty for each skipped character in the word + const skippedCharsCount = maxMatchColumn - patternLen; + result[0] -= skippedCharsCount; + + return result; } -function _doScore(pattern: string, patternLow: string, patternPos: number, patternStart: number, word: string, wordLow: string, wordPos: number) { - if (patternLow[patternPos] !== wordLow[wordPos]) { - return -1; +function _fillInMaxWordMatchPos(patternLen: number, wordLen: number, patternStart: number, wordStart: number, patternLow: string, wordLow: string) { + let patternPos = patternLen - 1; + let wordPos = wordLen - 1; + while (patternPos >= patternStart && wordPos >= wordStart) { + if (patternLow[patternPos] === wordLow[wordPos]) { + _maxWordMatchPos[patternPos] = wordPos; + patternPos--; + } + wordPos--; } +} + +function _doScore( + pattern: string, patternLow: string, patternPos: number, patternStart: number, + word: string, wordLow: string, wordPos: number, wordLen: number, wordStart: number, + newMatchStart: boolean, + outFirstMatchStrong: boolean[], +): number { + if (patternLow[patternPos] !== wordLow[wordPos]) { + return Number.MIN_SAFE_INTEGER; + } + + let score = 1; + let isGapLocation = false; if (wordPos === (patternPos - patternStart)) { // common prefix: `foobar <-> foobaz` // ^^^^^ - if (pattern[patternPos] === word[wordPos]) { - return 7; - } else { - return 5; - } + score = pattern[patternPos] === word[wordPos] ? 7 : 5; + } else if (isUpperCaseAtPos(wordPos, word, wordLow) && (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))) { // hitting upper-case: `foo <-> forOthers` // ^^ ^ - if (pattern[patternPos] === word[wordPos]) { - return 7; - } else { - return 5; - } + score = pattern[patternPos] === word[wordPos] ? 7 : 5; + isGapLocation = true; + } else if (isSeparatorAtPos(wordLow, wordPos) && (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))) { // hitting a separator: `. <-> foo.bar` // ^ - return 5; + score = 5; } else if (isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1)) { // post separator: `foo <-> bar_foo` // ^^^ - return 5; + score = 5; + isGapLocation = true; + } + if (score > 1 && patternPos === patternStart) { + outFirstMatchStrong[0] = true; + } + + if (!isGapLocation) { + isGapLocation = isUpperCaseAtPos(wordPos, word, wordLow) || isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1); + } + + // + if (patternPos === patternStart) { // first character in pattern + if (wordPos > wordStart) { + // the first pattern character would match a word character that is not at the word start + // so introduce a penalty to account for the gap preceding this match + score -= isGapLocation ? 3 : 5; + } } else { - return 1; - } -} - -let _matchesCount: number = 0; -let _topMatch2: number = 0; -let _topScore: number = 0; -let _wordStart: number = 0; -let _firstMatchCanBeWeak: boolean = false; - -function _findAllMatches2(row: number, column: number, total: number, matches: number, lastMatched: boolean): void { - - if (_matchesCount >= 10 || total < -25) { - // stop when having already 10 results, or - // when a potential alignment as already 5 gaps - return; - } - - let simpleMatchCount = 0; - - while (row > 0 && column > 0) { - - const score = _scores[row][column]; - const arrow = _arrows[row][column]; - - if (arrow === Arrow.Left) { - // left -> no match, skip a word character - column -= 1; - if (lastMatched) { - total -= 5; // new gap penalty - } else if (matches !== 0) { - total -= 1; // gap penalty after first match - } - lastMatched = false; - simpleMatchCount = 0; - - } else if (arrow & Arrow.Diag) { - - if (arrow & Arrow.Left) { - // left - _findAllMatches2( - row, - column - 1, - matches !== 0 ? total - 1 : total, // gap penalty after first match - matches, - lastMatched - ); - } - - // diag - total += score; - row -= 1; - column -= 1; - lastMatched = true; - - // match -> set a 1 at the word pos - matches += 2 ** (column + _wordStart); - - // count simple matches and boost a row of - // simple matches when they yield in a - // strong match. - if (score === 1) { - simpleMatchCount += 1; - - if (row === 0 && !_firstMatchCanBeWeak) { - // when the first match is a weak - // match we discard it - return undefined; - } - - } else { - // boost - total += 1 + (simpleMatchCount * (score - 1)); - simpleMatchCount = 0; - } - + if (newMatchStart) { + // this would be the beginning of a new match (i.e. there would be a gap before this location) + score += isGapLocation ? 2 : 0; } else { - return undefined; + // this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location + score += isGapLocation ? 0 : 1; } } - total -= column >= 3 ? 9 : column * 3; // late start penalty - - // dynamically keep track of the current top score - // and insert the current best score at head, the rest at tail - _matchesCount += 1; - if (total > _topScore) { - _topScore = total; - _topMatch2 = matches; + if (wordPos + 1 === wordLen) { + // we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word + // so pretend there is a gap after the last character in the word to normalize things + score -= isGapLocation ? 3 : 5; } + + return score; } //#endregion diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 685944f9f7..13485d0e8a 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -389,7 +389,7 @@ export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean // - description (if provided) // - query (normalized) // - number of query pieces (i.e. 'hello world' and 'helloworld' are different) - // - wether fuzzy matching is enabled or not + // - whether fuzzy matching is enabled or not let cacheHash: string; if (description) { cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index 5c7d5a7ea8..96a25833fd 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -393,15 +393,24 @@ function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern { } // common patterns: **/something/else just need endsWith check, something/else just needs and equals check -function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern { - const nativePath = paths.sep !== paths.posix.sep ? path.replace(ALL_FORWARD_SLASHES, paths.sep) : path; +function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern { + const usingPosixSep = paths.sep === paths.posix.sep; + const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, paths.sep); const nativePathEnd = paths.sep + nativePath; - const parsedPattern: ParsedStringPattern = matchPathEnds ? function (path, basename) { - return typeof path === 'string' && (path === nativePath || path.endsWith(nativePathEnd)) ? pattern : null; - } : function (path, basename) { - return typeof path === 'string' && path === nativePath ? pattern : null; + const targetPathEnd = paths.posix.sep + targetPath; + + const parsedPattern: ParsedStringPattern = matchPathEnds ? function (testPath, basename) { + return typeof testPath === 'string' && + ((testPath === nativePath || testPath.endsWith(nativePathEnd)) + || !usingPosixSep && (testPath === targetPath || testPath.endsWith(targetPathEnd))) + ? pattern : null; + } : function (testPath, basename) { + return typeof testPath === 'string' && + (testPath === nativePath + || (!usingPosixSep && testPath === targetPath)) + ? pattern : null; }; - parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + path]; + parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath]; return parsedPattern; } diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index 8890e04365..fe384023e1 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -5,7 +5,7 @@ import { equals } from 'vs/base/common/arrays'; import { UriComponents } from 'vs/base/common/uri'; -import { escapeCodicons } from 'vs/base/common/codicons'; +import { escapeIcons } from 'vs/base/common/iconLabels'; import { illegalArgument } from 'vs/base/common/errors'; export interface IMarkdownString { @@ -46,9 +46,7 @@ export class MarkdownString implements IMarkdownString { } appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString { - // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash - this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) - .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value) .replace(/([ \t]+)/g, (_match, g1) => ' '.repeat(g1.length)) .replace(/^>/gm, '\\>') .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n'); @@ -116,6 +114,11 @@ function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { } } +export function escapeMarkdownSyntaxTokens(text: string): string { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); +} + export function removeMarkdownEscapes(text: string): string { if (!text) { return text; diff --git a/src/vs/base/common/iconLabels.ts b/src/vs/base/common/iconLabels.ts new file mode 100644 index 0000000000..f36d9d3319 --- /dev/null +++ b/src/vs/base/common/iconLabels.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CSSIcon } from 'vs/base/common/codicons'; +import { matchesFuzzy, IMatch } from 'vs/base/common/filters'; +import { ltrim } from 'vs/base/common/strings'; + +export const iconStartMarker = '$('; + +const iconsRegex = new RegExp(`\\$\\(${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?\\)`, 'g'); // no capturing groups + +const escapeIconsRegex = new RegExp(`(\\\\)?${iconsRegex.source}`, 'g'); +export function escapeIcons(text: string): string { + return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`); +} + +const markdownEscapedIconsRegex = new RegExp(`\\\\${iconsRegex.source}`, 'g'); +export function markdownEscapeEscapedIcons(text: string): string { + // Need to add an extra \ for escaping in markdown + return text.replace(markdownEscapedIconsRegex, match => `\\${match}`); +} + +const stripIconsRegex = new RegExp(`(\\s)?(\\\\)?${iconsRegex.source}(\\s)?`, 'g'); +export function stripIcons(text: string): string { + if (text.indexOf(iconStartMarker) === -1) { + return text; + } + + return text.replace(stripIconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || ''); +} + + +export interface IParsedLabelWithIcons { + readonly text: string; + readonly iconOffsets?: readonly number[]; +} + +export function parseLabelWithIcons(text: string): IParsedLabelWithIcons { + const firstIconIndex = text.indexOf(iconStartMarker); + if (firstIconIndex === -1) { + return { text }; // return early if the word does not include an icon + } + + return doParseLabelWithIcons(text, firstIconIndex); +} + +function doParseLabelWithIcons(text: string, firstIconIndex: number): IParsedLabelWithIcons { + const iconOffsets: number[] = []; + let textWithoutIcons: string = ''; + + function appendChars(chars: string) { + if (chars) { + textWithoutIcons += chars; + + for (const _ of chars) { + iconOffsets.push(iconsOffset); // make sure to fill in icon offsets + } + } + } + + let currentIconStart = -1; + let currentIconValue: string = ''; + let iconsOffset = 0; + + let char: string; + let nextChar: string; + + let offset = firstIconIndex; + const length = text.length; + + // Append all characters until the first icon + appendChars(text.substr(0, firstIconIndex)); + + // example: $(file-symlink-file) my cool $(other-icon) entry + while (offset < length) { + char = text[offset]; + nextChar = text[offset + 1]; + + // beginning of icon: some value $( <-- + if (char === iconStartMarker[0] && nextChar === iconStartMarker[1]) { + currentIconStart = offset; + + // if we had a previous potential icon value without + // the closing ')', it was actually not an icon and + // so we have to add it to the actual value + appendChars(currentIconValue); + + currentIconValue = iconStartMarker; + + offset++; // jump over '(' + } + + // end of icon: some value $(some-icon) <-- + else if (char === ')' && currentIconStart !== -1) { + const currentIconLength = offset - currentIconStart + 1; // +1 to include the closing ')' + iconsOffset += currentIconLength; + currentIconStart = -1; + currentIconValue = ''; + } + + // within icon + else if (currentIconStart !== -1) { + // Make sure this is a real icon name + if (/^[a-z0-9\-]$/i.test(char)) { + currentIconValue += char; + } else { + // This is not a real icon, treat it as text + appendChars(currentIconValue); + + currentIconStart = -1; + currentIconValue = ''; + } + } + + // any value outside of icon + else { + appendChars(char); + } + + offset++; + } + + // if we had a previous potential icon value without + // the closing ')', it was actually not an icon and + // so we have to add it to the actual value + appendChars(currentIconValue); + + return { text: textWithoutIcons, iconOffsets }; +} + +export function matchesFuzzyIconAware(query: string, target: IParsedLabelWithIcons, enableSeparateSubstringMatching = false): IMatch[] | null { + const { text, iconOffsets } = target; + + // Return early if there are no icon markers in the word to match against + if (!iconOffsets || iconOffsets.length === 0) { + return matchesFuzzy(query, text, enableSeparateSubstringMatching); + } + + // Trim the word to match against because it could have leading + // whitespace now if the word started with an icon + const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' '); + const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length; + + // match on value without icon + const matches = matchesFuzzy(query, wordToMatchAgainstWithoutIconsTrimmed, enableSeparateSubstringMatching); + + // Map matches back to offsets with icon and trimming + if (matches) { + for (const match of matches) { + const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; + match.start += iconOffset; + match.end += iconOffset; + } + } + + return matches; +} diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 3015e91c2e..34feb500f7 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -22,6 +22,10 @@ export namespace Iterable { return iterable || _empty; } + export function isEmpty(iterable: Iterable | undefined | null): boolean { + return !iterable || iterable[Symbol.iterator]().next().done === true; + } + export function first(iterable: Iterable): T | undefined { return iterable[Symbol.iterator]().next().value; } @@ -35,6 +39,8 @@ export namespace Iterable { return false; } + export function filter(iterable: Iterable, predicate: (t: T) => t is R): Iterable; + export function filter(iterable: Iterable, predicate: (t: T) => boolean): Iterable; export function* filter(iterable: Iterable, predicate: (t: T) => boolean): Iterable { for (const element of iterable) { if (predicate(element)) { @@ -57,6 +63,41 @@ export namespace Iterable { } } + export function* concatNested(iterables: Iterable>): Iterable { + for (const iterable of iterables) { + for (const element of iterable) { + yield element; + } + } + } + + export function reduce(iterable: Iterable, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R { + let value = initialValue; + for (const element of iterable) { + value = reducer(value, element); + } + return value; + } + + /** + * Returns an iterable slice of the array, with the same semantics as `array.slice()`. + */ + export function* slice(iterable: ReadonlyArray, from: number, to = iterable.length): Iterable { + if (from < 0) { + from += iterable.length; + } + + if (to < 0) { + to += iterable.length; + } else if (to > iterable.length) { + to = iterable.length; + } + + for (; from < to; from++) { + yield iterable[from]; + } + } + /** * Consumes `atMost` elements from iterable and returns the consumed elements, * and an iterable for the rest of the elements. diff --git a/src/vs/base/common/keyCodes.ts b/src/vs/base/common/keyCodes.ts index 631c3ad06f..c082d6e7d4 100644 --- a/src/vs/base/common/keyCodes.ts +++ b/src/vs/base/common/keyCodes.ts @@ -597,6 +597,17 @@ export abstract class ResolvedKeybinding { /** * Returns the parts that should be used for dispatching. + * Returns null for parts consisting of only modifier keys + * @example keybinding "Shift" -> null + * @example keybinding ("D" with shift == true) -> "shift+D" */ public abstract getDispatchParts(): (string | null)[]; + + /** + * Returns the parts that should be used for dispatching single modifier keys + * Returns null for parts that contain more than one modifier or a regular key. + * @example keybinding "Shift" -> "shift" + * @example keybinding ("D" with shift == true") -> null + */ + public abstract getSingleModifierDispatchParts(): (string | null)[]; } diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index d9fd2dfbd7..3a0ff19d4c 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -9,11 +9,12 @@ import { startsWithIgnoreCase, rtrim } from 'vs/base/common/strings'; import { Schemas } from 'vs/base/common/network'; import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform'; import { isEqual, basename, relativePath } from 'vs/base/common/resources'; +import { hasDriveLetter, isRootOrDriveLetter } from 'vs/base/common/extpath'; export interface IWorkspaceFolderProvider { - getWorkspaceFolder(resource: URI): { uri: URI, name?: string } | null; + getWorkspaceFolder(resource: URI): { uri: URI, name?: string; } | null; getWorkspace(): { - folders: { uri: URI, name?: string }[]; + folders: { uri: URI, name?: string; }[]; }; } @@ -84,21 +85,13 @@ export function getBaseLabel(resource: URI | string | undefined): string | undef const base = basename(resource) || (resource.scheme === Schemas.file ? resource.fsPath : resource.path) /* can be empty string if '/' is passed in */; // convert c: => C: - if (hasDriveLetter(base)) { + if (isWindows && isRootOrDriveLetter(base)) { return normalizeDriveLetter(base); } return base; } -function hasDriveLetter(path: string): boolean { - return !!(isWindows && path && path[1] === ':'); -} - -export function extractDriveLetter(path: string): string | undefined { - return hasDriveLetter(path) ? path[0] : undefined; -} - export function normalizeDriveLetter(path: string): string { if (hasDriveLetter(path)) { return path.charAt(0).toUpperCase() + path.slice(1); @@ -107,7 +100,7 @@ export function normalizeDriveLetter(path: string): string { return path; } -let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null); +let normalizedUserHomeCached: { original: string; normalized: string; } = Object.create(null); export function tildify(path: string, userHome: string): string { if (isWindows || !path || !userHome) { return path; // unsupported @@ -286,7 +279,7 @@ interface ISegment { * @param value string to which templating is applied * @param values the values of the templates to use */ -export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null } = Object.create(null)): string { +export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null; } = Object.create(null)): string { const segments: ISegment[] = []; let inVariable = false; @@ -390,7 +383,7 @@ export function unmnemonicLabel(label: string): string { /** * Splits a path in name and parent path, supporting both '/' and '\' */ -export function splitName(fullPath: string): { name: string, parentPath: string } { +export function splitName(fullPath: string): { name: string, parentPath: string; } { const p = fullPath.indexOf('/') !== -1 ? posix : win32; const name = p.basename(fullPath); const parentPath = p.dirname(fullPath); diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index c7b00212cc..bb57ebc584 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -14,34 +14,53 @@ import { Iterable } from 'vs/base/common/iterator'; * extend Disposable or use a DisposableStore. This means there are a lot of false positives. */ const TRACK_DISPOSABLES = false; +let disposableTracker: IDisposableTracker | null = null; -const __is_disposable_tracked__ = '__is_disposable_tracked__'; - -function markTracked(x: T): void { - if (!TRACK_DISPOSABLES) { - return; - } - - if (x && x !== Disposable.None) { - try { - (x as any)[__is_disposable_tracked__] = true; - } catch { - // noop - } - } +export interface IDisposableTracker { + trackDisposable(x: IDisposable): void; + markTracked(x: IDisposable): void; } -function trackDisposable(x: T): T { - if (!TRACK_DISPOSABLES) { +export function setDisposableTracker(tracker: IDisposableTracker | null): void { + disposableTracker = tracker; +} + +if (TRACK_DISPOSABLES) { + const __is_disposable_tracked__ = '__is_disposable_tracked__'; + disposableTracker = new class implements IDisposableTracker { + trackDisposable(x: IDisposable): void { + const stack = new Error('Potentially leaked disposable').stack!; + setTimeout(() => { + if (!(x as any)[__is_disposable_tracked__]) { + console.log(stack); + } + }, 3000); + } + + markTracked(x: IDisposable): void { + if (x && x !== Disposable.None) { + try { + (x as any)[__is_disposable_tracked__] = true; + } catch { + // noop + } + } + } + }; +} + +function markTracked(x: T): void { + if (!disposableTracker) { + return; + } + disposableTracker.markTracked(x); +} + +export function trackDisposable(x: T): T { + if (!disposableTracker) { return x; } - - const stack = new Error('Potentially leaked disposable').stack!; - setTimeout(() => { - if (!(x as any)[__is_disposable_tracked__]) { - console.log(stack); - } - }, 3000); + disposableTracker.trackDisposable(x); return x; } @@ -49,7 +68,7 @@ export class MultiDisposeError extends Error { constructor( public readonly errors: any[] ) { - super(`Encounter errors while disposing of store. Errors: [${errors.join(', ')}]`); + super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`); } } @@ -98,7 +117,7 @@ export function dispose(arg: T | IterableIterator | un export function combinedDisposable(...disposables: IDisposable[]): IDisposable { disposables.forEach(markTracked); - return trackDisposable({ dispose: () => dispose(disposables) }); + return toDisposable(() => dispose(disposables)); } export function toDisposable(fn: () => void): IDisposable { @@ -212,9 +231,7 @@ export class MutableDisposable implements IDisposable { return; } - if (this._value) { - this._value.dispose(); - } + this._value?.dispose(); if (value) { markTracked(value); } @@ -228,9 +245,7 @@ export class MutableDisposable implements IDisposable { dispose(): void { this._isDisposed = true; markTracked(this); - if (this._value) { - this._value.dispose(); - } + this._value?.dispose(); this._value = undefined; } } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 6251e23d49..31889c0334 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -377,7 +377,8 @@ export class TernarySearchTree { } has(key: K): boolean { - return !!this._getNode(key); + const node = this._getNode(key); + return !(node?.value === undefined && node?.mid === undefined); } delete(key: K): void { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 127d0c286a..1241b1054c 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -60,6 +60,8 @@ export namespace Schemas { export const vscodeNotebookCell = 'vscode-notebook-cell'; + export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata'; + export const vscodeSettings = 'vscode-settings'; export const webviewPanel = 'webview-panel'; @@ -147,8 +149,8 @@ class FileAccessImpl { * **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context. */ asBrowserUri(uri: URI): URI; - asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI; - asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI { + asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }, __forceCodeFileUri?: boolean): URI; + asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }, __forceCodeFileUri?: boolean): URI { const uri = this.toUri(uriOrModule, moduleIdToUrl); // Handle remote URIs via `RemoteAuthorities` @@ -157,37 +159,23 @@ class FileAccessImpl { } // Only convert the URI if we are in a native context and it has `file:` scheme - if (platform.isElectronSandboxed && platform.isNative && uri.scheme === Schemas.file) { - return this.toCodeFileUri(uri); + // and we have explicitly enabled the conversion (sandbox, or ENABLE_VSCODE_BROWSER_CODE_LOADING) + if (platform.isNative && (__forceCodeFileUri || platform.isPreferringBrowserCodeLoad) && uri.scheme === Schemas.file) { + return uri.with({ + scheme: Schemas.vscodeFileResource, + // We need to provide an authority here so that it can serve + // as origin for network and loading matters in chromium. + // If the URI is not coming with an authority already, we + // add our own + authority: uri.authority || this.FALLBACK_AUTHORITY, + query: null, + fragment: null + }); } return uri; } - /** - * TODO@bpasero remove me eventually when vscode-file is adopted everywhere - */ - _asCodeFileUri(uri: URI): URI; - _asCodeFileUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI; - _asCodeFileUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI { - const uri = this.toUri(uriOrModule, moduleIdToUrl); - - return this.toCodeFileUri(uri); - } - - private toCodeFileUri(uri: URI): URI { - return uri.with({ - scheme: Schemas.vscodeFileResource, - // We need to provide an authority here so that it can serve - // as origin for network and loading matters in chromium. - // If the URI is not coming with an authority already, we - // add our own - authority: uri.authority || this.FALLBACK_AUTHORITY, - query: null, - fragment: null - }); - } - /** * Returns the `file` URI to use in contexts where node.js * is responsible for loading. diff --git a/src/vs/base/common/paging.ts b/src/vs/base/common/paging.ts index 8657515d72..bdab279bae 100644 --- a/src/vs/base/common/paging.ts +++ b/src/vs/base/common/paging.ts @@ -187,18 +187,3 @@ export function mapPager(pager: IPager, fn: (t: T) => R): IPager { getPage: (pageIndex, token) => pager.getPage(pageIndex, token).then(r => r.map(fn)) }; } - -/** - * Merges two pagers. - */ -export function mergePagers(one: IPager, other: IPager): IPager { - return { - firstPage: [...one.firstPage, ...other.firstPage], - total: one.total + other.total, - pageSize: one.pageSize + other.pageSize, - getPage(pageIndex: number, token): Promise { - return Promise.all([one.getPage(pageIndex, token), other.getPage(pageIndex, token)]) - .then(([onePage, otherPage]) => [...onePage, ...otherPage]); - } - }; -} \ No newline at end of file diff --git a/src/vs/base/common/path.ts b/src/vs/base/common/path.ts index 15a215f401..5ea4724e16 100644 --- a/src/vs/base/common/path.ts +++ b/src/vs/base/common/path.ts @@ -43,7 +43,7 @@ const CHAR_QUESTION_MARK = 63; /* ? */ class ErrorInvalidArgType extends Error { code: 'ERR_INVALID_ARG_TYPE'; - constructor(name: string, expected: string, actual: any) { + constructor(name: string, expected: string, actual: unknown) { // determiner: 'must be' or 'must not be' let determiner; if (typeof expected === 'string' && expected.indexOf('not ') === 0) { @@ -215,7 +215,7 @@ export const win32: IPath = { // absolute path, get cwd for that drive, or the process cwd if // the drive cwd is not available. We're sure the device is not // a UNC path at this points, because UNC paths are always absolute. - path = (process.env as any)[`=${resolvedDevice}`] || process.cwd(); + path = process.env[`=${resolvedDevice}`] || process.cwd(); // Verify that a cwd was found and that it actually points // to our drive. If not, default to the drive's root. diff --git a/src/vs/base/common/performance.d.ts b/src/vs/base/common/performance.d.ts index b48941728b..4eed08846b 100644 --- a/src/vs/base/common/performance.d.ts +++ b/src/vs/base/common/performance.d.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export interface PerformanceEntry { +export interface PerformanceMark { readonly name: string; readonly startTime: number; } @@ -11,12 +11,6 @@ export interface PerformanceEntry { export function mark(name: string): void; /** - * All entries filtered by type and sorted by `startTime`. + * Returns all marks, sorted by `startTime`. */ -export function getEntries(): PerformanceEntry[]; - -export function getDuration(from: string, to: string): number; - -type ExportData = any[]; -export function importEntries(data: ExportData): void; -export function exportEntries(): ExportData; +export function getMarks(): PerformanceMark[]; diff --git a/src/vs/base/common/performance.js b/src/vs/base/common/performance.js index a520a9434e..1e372defe4 100644 --- a/src/vs/base/common/performance.js +++ b/src/vs/base/common/performance.js @@ -7,66 +7,88 @@ //@ts-check -function _factory(sharedObj) { +/** + * @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}} + */ +function _definePolyfillMarks(timeOrigin) { - sharedObj.MonacoPerformanceMarks = sharedObj.MonacoPerformanceMarks || []; - - const _dataLen = 2; - const _nativeMark = typeof performance === 'object' && typeof performance.mark === 'function' ? performance.mark.bind(performance) : () => { }; - - function importEntries(entries) { - sharedObj.MonacoPerformanceMarks.splice(0, 0, ...entries); + const _data = []; + if (typeof timeOrigin === 'number') { + _data.push('code/timeOrigin', timeOrigin); } - function exportEntries() { - return sharedObj.MonacoPerformanceMarks.slice(0); + function mark(name) { + _data.push(name, Date.now()); } - - function getEntries() { + function getMarks() { const result = []; - const entries = sharedObj.MonacoPerformanceMarks; - for (let i = 0; i < entries.length; i += _dataLen) { + for (let i = 0; i < _data.length; i += 2) { result.push({ - name: entries[i], - startTime: entries[i + 1], + name: _data[i], + startTime: _data[i + 1], }); } return result; } + return { mark, getMarks }; +} - function getDuration(from, to) { - const entries = sharedObj.MonacoPerformanceMarks; - let target = to; - let endIndex = 0; - for (let i = entries.length - _dataLen; i >= 0; i -= _dataLen) { - if (entries[i] === target) { - if (target === to) { - // found `to` (end of interval) - endIndex = i; - target = from; - } else { - // found `from` (start of interval) - return entries[endIndex + 1] - entries[i + 1]; +/** + * @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}} + */ +function _define() { + + if (typeof performance === 'object' && typeof performance.mark === 'function') { + // in a browser context, reuse performance-util + + if (typeof performance.timeOrigin !== 'number' && !performance.timing) { + // safari & webworker: because there is no timeOrigin and no workaround + // we use the `Date.now`-based polyfill. + return _definePolyfillMarks(); + + } else { + // use "native" performance for mark and getMarks + return { + mark(name) { + performance.mark(name); + }, + getMarks() { + let timeOrigin = performance.timeOrigin; + if (typeof timeOrigin !== 'number') { + // safari: there is no timerOrigin but in renderers there is the timing-property + // see https://bugs.webkit.org/show_bug.cgi?id=174862 + timeOrigin = performance.timing.navigationStart || performance.timing.redirectStart || performance.timing.fetchStart; + } + const result = [{ name: 'code/timeOrigin', startTime: Math.round(timeOrigin) }]; + for (const entry of performance.getEntriesByType('mark')) { + result.push({ + name: entry.name, + startTime: Math.round(timeOrigin + entry.startTime) + }); + } + return result; } - } + }; } - return 0; + + } else if (typeof process === 'object') { + // node.js: use the normal polyfill but add the timeOrigin + // from the node perf_hooks API as very first mark + const timeOrigin = Math.round((require.nodeRequire || require)('perf_hooks').performance.timeOrigin); + return _definePolyfillMarks(timeOrigin); + + } else { + // unknown environment + console.trace('perf-util loaded in UNKNOWN environment'); + return _definePolyfillMarks(); } +} - function mark(name) { - sharedObj.MonacoPerformanceMarks.push(name, Date.now()); - _nativeMark(name); +function _factory(sharedObj) { + if (!sharedObj.MonacoPerformanceMarks) { + sharedObj.MonacoPerformanceMarks = _define(); } - - const exports = { - mark: mark, - getEntries: getEntries, - getDuration: getDuration, - importEntries: importEntries, - exportEntries: exportEntries - }; - - return exports; + return sharedObj.MonacoPerformanceMarks; } // This module can be loaded in an amd and commonjs-context. @@ -92,5 +114,6 @@ if (typeof define === 'function') { // commonjs module.exports = _factory(sharedObj); } else { + console.trace('perf-util defined in UNKNOWN context (neither requirejs or commonjs)'); sharedObj.perf = _factory(sharedObj); } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index e74679b8b7..f1f93a2c37 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -8,6 +8,7 @@ const LANGUAGE_DEFAULT = 'en'; let _isWindows = false; let _isMacintosh = false; let _isLinux = false; +let _isLinuxSnap = false; let _isNative = false; let _isWeb = false; let _isIOS = false; @@ -61,6 +62,26 @@ if (typeof process !== 'undefined') { const isElectronRenderer = typeof nodeProcess?.versions?.electron === 'string' && nodeProcess.type === 'renderer'; export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed; +export const browserCodeLoadingCacheStrategy: 'none' | 'code' | 'bypassHeatCheck' | 'bypassHeatCheckAndEagerCompile' | undefined = (() => { + + // Always enabled when sandbox is enabled + if (isElectronSandboxed) { + return 'bypassHeatCheck'; + } + + // Otherwise, only enabled conditionally + const env = nodeProcess?.env['ENABLE_VSCODE_BROWSER_CODE_LOADING']; + if (typeof env === 'string') { + if (env === 'none' || env === 'code' || env === 'bypassHeatCheck' || env === 'bypassHeatCheckAndEagerCompile') { + return env; + } + + return 'bypassHeatCheck'; + } + + return undefined; +})(); +export const isPreferringBrowserCodeLoad = typeof browserCodeLoadingCacheStrategy === 'string'; // Web environment if (typeof navigator === 'object' && !isElectronRenderer) { @@ -79,6 +100,7 @@ else if (typeof nodeProcess === 'object') { _isWindows = (nodeProcess.platform === 'win32'); _isMacintosh = (nodeProcess.platform === 'darwin'); _isLinux = (nodeProcess.platform === 'linux'); + _isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION']; _locale = LANGUAGE_DEFAULT; _language = LANGUAGE_DEFAULT; const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG']; @@ -128,6 +150,7 @@ if (_isMacintosh) { export const isWindows = _isWindows; export const isMacintosh = _isMacintosh; export const isLinux = _isLinux; +export const isLinuxSnap = _isLinuxSnap; export const isNative = _isNative; export const isWeb = _isWeb; export const isIOS = _isIOS; @@ -212,7 +235,7 @@ export const setImmediate: ISetImmediate = (function defineSetImmediate() { globals.postMessage({ vscodeSetImmediateId: myId }, '*'); }; } - if (nodeProcess) { + if (nodeProcess && typeof nodeProcess.nextTick === 'function') { return nodeProcess.nextTick.bind(nodeProcess); } const _promise = Promise.resolve(); diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index c1d28bf127..3b898aaf05 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -108,7 +108,6 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve }, {} as Record); const keysToRemove = [ /^ELECTRON_.+$/, - /^GOOGLE_API_KEY$/, /^VSCODE_.+$/, /^SNAP(|_.*)$/, /^GDK_PIXBUF_.+$/, diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 8e378414a6..8cd1cb21ab 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -42,8 +42,8 @@ export interface IExtUri { /** * Tests whether a `candidate` URI is a parent or equal of a given `base` URI. * - * @param base A uri which is "longer" - * @param parentCandidate A uri which is "shorter" then `base` + * @param base A uri which is "longer" or at least same length as `parentCandidate` + * @param parentCandidate A uri which is "shorter" or up to same length as `base` * @param ignoreFragment Ignore the fragment (defaults to `false`) */ isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean; diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index 91a47c48d1..6745114c5e 100644 --- a/src/vs/base/common/scrollable.ts +++ b/src/vs/base/common/scrollable.ts @@ -13,6 +13,8 @@ export const enum ScrollbarVisibility { } export interface ScrollEvent { + inSmoothScrolling: boolean; + oldWidth: number; oldScrollWidth: number; oldScrollLeft: number; @@ -132,7 +134,7 @@ export class ScrollState implements IScrollDimensions, IScrollPosition { ); } - public createScrollEvent(previous: ScrollState): ScrollEvent { + public createScrollEvent(previous: ScrollState, inSmoothScrolling: boolean): ScrollEvent { const widthChanged = (this.width !== previous.width); const scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth); const scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft); @@ -142,6 +144,7 @@ export class ScrollState implements IScrollDimensions, IScrollPosition { const scrollTopChanged = (this.scrollTop !== previous.scrollTop); return { + inSmoothScrolling: inSmoothScrolling, oldWidth: previous.width, oldScrollWidth: previous.scrollWidth, oldScrollLeft: previous.scrollLeft, @@ -242,7 +245,7 @@ export class Scrollable extends Disposable { public setScrollDimensions(dimensions: INewScrollDimensions, useRawScrollPositions: boolean): void { const newState = this._state.withScrollDimensions(dimensions, useRawScrollPositions); - this._setState(newState); + this._setState(newState, Boolean(this._smoothScrolling)); // Validate outstanding animated scroll position target if (this._smoothScrolling) { @@ -279,10 +282,10 @@ export class Scrollable extends Disposable { this._smoothScrolling = null; } - this._setState(newState); + this._setState(newState, false); } - public setScrollPositionSmooth(update: INewScrollPosition): void { + public setScrollPositionSmooth(update: INewScrollPosition, reuseAnimation?: boolean): void { if (this._smoothScrollDuration === 0) { // Smooth scrolling not supported. return this.setScrollPositionNow(update); @@ -302,8 +305,12 @@ export class Scrollable extends Disposable { // No need to interrupt or extend the current animation since we're going to the same place return; } - - const newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration); + let newSmoothScrolling: SmoothScrollingOperation; + if (reuseAnimation) { + newSmoothScrolling = new SmoothScrollingOperation(this._smoothScrolling.from, validTarget, this._smoothScrolling.startTime, this._smoothScrolling.duration); + } else { + newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration); + } this._smoothScrolling.dispose(); this._smoothScrolling = newSmoothScrolling; } else { @@ -330,7 +337,7 @@ export class Scrollable extends Disposable { const update = this._smoothScrolling.tick(); const newState = this._state.withScrollPosition(update); - this._setState(newState); + this._setState(newState, true); if (!this._smoothScrolling) { // Looks like someone canceled the smooth scrolling @@ -354,14 +361,14 @@ export class Scrollable extends Disposable { }); } - private _setState(newState: ScrollState): void { + private _setState(newState: ScrollState, inSmoothScrolling: boolean): void { const oldState = this._state; if (oldState.equals(newState)) { // no change return; } this._state = newState; - this._onScroll.fire(this._state.createScrollEvent(oldState)); + this._onScroll.fire(this._state.createScrollEvent(oldState, inSmoothScrolling)); } } @@ -404,17 +411,17 @@ export class SmoothScrollingOperation { public readonly from: ISmoothScrollPosition; public to: ISmoothScrollPosition; public readonly duration: number; - private readonly _startTime: number; + public readonly startTime: number; public animationFrameDisposable: IDisposable | null; private scrollLeft!: IAnimation; private scrollTop!: IAnimation; - protected constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) { + constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) { this.from = from; this.to = to; this.duration = duration; - this._startTime = startTime; + this.startTime = startTime; this.animationFrameDisposable = null; @@ -460,7 +467,7 @@ export class SmoothScrollingOperation { } protected _tick(now: number): SmoothScrollingUpdate { - const completion = (now - this._startTime) / this.duration; + const completion = (now - this.startTime) / this.duration; if (completion < 1) { const newScrollLeft = this.scrollLeft(completion); diff --git a/src/vs/base/common/stopwatch.ts b/src/vs/base/common/stopwatch.ts index 7f393c0eef..f6fe509128 100644 --- a/src/vs/base/common/stopwatch.ts +++ b/src/vs/base/common/stopwatch.ts @@ -35,6 +35,6 @@ export class StopWatch { } private _now(): number { - return this._highResolution ? globals.performance.now() : new Date().getTime(); + return this._highResolution ? globals.performance.now() : Date.now(); } } diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index 27ac18101a..542f0df42f 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; /** @@ -229,7 +230,7 @@ class WriteableStreamImpl implements WriteableStream { // flowing: directly send the data to listeners if (this.state.flowing) { - this.listeners.data.forEach(listener => listener(data)); + this.emitData(data); } // not yet flowing: buffer data until flowing @@ -250,7 +251,7 @@ class WriteableStreamImpl implements WriteableStream { // flowing: directly send the error to listeners if (this.state.flowing) { - this.listeners.error.forEach(listener => listener(error)); + this.emitError(error); } // not yet flowing: buffer errors until flowing @@ -273,7 +274,7 @@ class WriteableStreamImpl implements WriteableStream { // flowing: send end event to listeners if (this.state.flowing) { - this.listeners.end.forEach(listener => listener()); + this.emitEnd(); this.destroy(); } @@ -284,6 +285,22 @@ class WriteableStreamImpl implements WriteableStream { } } + private emitData(data: T): void { + this.listeners.data.slice(0).forEach(listener => listener(data)); // slice to avoid listener mutation from delivering event + } + + private emitError(error: Error): void { + if (this.listeners.error.length === 0) { + onUnexpectedError(error); // nobody listened to this error so we log it as unexpected + } else { + this.listeners.error.slice(0).forEach(listener => listener(error)); // slice to avoid listener mutation from delivering event + } + } + + private emitEnd(): void { + this.listeners.end.slice(0).forEach(listener => listener()); // slice to avoid listener mutation from delivering event + } + on(event: 'data', callback: (data: T) => void): void; on(event: 'error', callback: (err: Error) => void): void; on(event: 'end', callback: () => void): void; @@ -361,7 +378,7 @@ class WriteableStreamImpl implements WriteableStream { if (this.buffer.data.length > 0) { const fullDataBuffer = this.reducer(this.buffer.data); - this.listeners.data.forEach(listener => listener(fullDataBuffer)); + this.emitData(fullDataBuffer); this.buffer.data.length = 0; @@ -375,7 +392,7 @@ class WriteableStreamImpl implements WriteableStream { private flowErrors(): void { if (this.listeners.error.length > 0) { for (const error of this.buffer.error) { - this.listeners.error.forEach(listener => listener(error)); + this.emitError(error); } this.buffer.error.length = 0; @@ -384,7 +401,7 @@ class WriteableStreamImpl implements WriteableStream { private flowEnd(): boolean { if (this.state.ended) { - this.listeners.end.forEach(listener => listener()); + this.emitEnd(); return this.listeners.end.length > 0; } @@ -574,3 +591,40 @@ export function transform(stream: ReadableStreamEvents Promise; +} + +/** + * Helper to observe a stream for certain events through + * a promise based API. + */ +export function observe(stream: ReadableStream): IReadableStreamObservable { + + // A stream is closed when it ended or errord + // We install this listener right from the + // beginning to catch the events early. + const errorOrEnd = Promise.race([ + new Promise(resolve => stream.on('end', () => resolve())), + new Promise(resolve => stream.on('error', () => resolve())) + ]); + + return { + errorOrEnd(): Promise { + + // We need to ensure the stream is flowing so that our + // listeners are getting triggered. It is possible that + // the stream is not flowing because no `data` listener + // was attached yet. + stream.resume(); + + return errorOrEnd; + } + }; +} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index a46b6da14e..32cdc31789 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -712,7 +712,7 @@ export function decodeUTF8(buffer: Uint8Array): string { } /** - * Generated using https://github.com/alexandrudima/unicode-utils/blob/master/generate-rtl-test.js + * Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-rtl-test.js */ const CONTAINS_RTL = /(?:[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05F4\u0608\u060B\u060D\u061B-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u0710\u0712-\u072F\u074D-\u07A5\u07B1-\u07EA\u07F4\u07F5\u07FA-\u0815\u081A\u0824\u0828\u0830-\u0858\u085E-\u08BD\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFD3D\uFD50-\uFDFC\uFE70-\uFEFC]|\uD802[\uDC00-\uDD1B\uDD20-\uDE00\uDE10-\uDE33\uDE40-\uDEE4\uDEEB-\uDF35\uDF40-\uDFFF]|\uD803[\uDC00-\uDCFF]|\uD83A[\uDC00-\uDCCF\uDD00-\uDD43\uDD50-\uDFFF]|\uD83B[\uDC00-\uDEBB])/; @@ -724,9 +724,9 @@ export function containsRTL(str: string): boolean { } /** - * Generated using https://github.com/alexandrudima/unicode-utils/blob/master/generate-emoji-test.js + * Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js */ -const CONTAINS_EMOJI = /(?:[\u231A\u231B\u23F0\u23F3\u2600-\u27BF\u2B50\u2B55]|\uD83C[\uDDE6-\uDDFF\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F\uDE80-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD00-\uDDFF\uDE70-\uDE73\uDE78-\uDE82\uDE90-\uDE95])/; +const CONTAINS_EMOJI = /(?:[\u231A\u231B\u23F0\u23F3\u2600-\u27BF\u2B50\u2B55]|\uD83C[\uDDE6-\uDDFF\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F\uDE80-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD00-\uDDFF\uDE70-\uDED6])/; export function containsEmoji(str: string): boolean { return CONTAINS_EMOJI.test(str); @@ -806,13 +806,15 @@ export function isFullWidthCharacter(charCode: number): boolean { /** * A fast function (therefore imprecise) to check if code points are emojis. - * Generated using https://github.com/alexandrudima/unicode-utils/blob/master/generate-emoji-test.js + * Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js */ export function isEmojiImprecise(x: number): boolean { return ( - (x >= 0x1F1E6 && x <= 0x1F1FF) || (x >= 9728 && x <= 10175) || (x >= 127744 && x <= 128591) - || (x >= 128640 && x <= 128764) || (x >= 128992 && x <= 129003) || (x >= 129280 && x <= 129535) - || (x >= 129648 && x <= 129651) || (x >= 129656 && x <= 129666) || (x >= 129680 && x <= 129685) + (x >= 0x1F1E6 && x <= 0x1F1FF) || (x === 8986) || (x === 8987) || (x === 9200) + || (x === 9203) || (x >= 9728 && x <= 10175) || (x === 11088) || (x === 11093) + || (x >= 127744 && x <= 128591) || (x >= 128640 && x <= 128764) + || (x >= 128992 && x <= 129003) || (x >= 129280 && x <= 129535) + || (x >= 129648 && x <= 129750) ); } @@ -1122,7 +1124,7 @@ class GraphemeBreakTree { } function getGraphemeBreakRawData(): number[] { - // generated using https://github.com/alexandrudima/unicode-utils/blob/master/generate-grapheme-break.js + // generated using https://github.com/alexdima/unicode-utils/blob/master/generate-grapheme-break.js return JSON.parse('[0,0,0,51592,51592,11,44424,44424,11,72251,72254,5,7150,7150,7,48008,48008,11,55176,55176,11,128420,128420,14,3276,3277,5,9979,9980,14,46216,46216,11,49800,49800,11,53384,53384,11,70726,70726,5,122915,122916,5,129320,129327,14,2558,2558,5,5906,5908,5,9762,9763,14,43360,43388,8,45320,45320,11,47112,47112,11,48904,48904,11,50696,50696,11,52488,52488,11,54280,54280,11,70082,70083,1,71350,71350,7,73111,73111,5,127892,127893,14,128726,128727,14,129473,129474,14,2027,2035,5,2901,2902,5,3784,3789,5,6754,6754,5,8418,8420,5,9877,9877,14,11088,11088,14,44008,44008,5,44872,44872,11,45768,45768,11,46664,46664,11,47560,47560,11,48456,48456,11,49352,49352,11,50248,50248,11,51144,51144,11,52040,52040,11,52936,52936,11,53832,53832,11,54728,54728,11,69811,69814,5,70459,70460,5,71096,71099,7,71998,71998,5,72874,72880,5,119149,119149,7,127374,127374,14,128335,128335,14,128482,128482,14,128765,128767,14,129399,129400,14,129680,129685,14,1476,1477,5,2377,2380,7,2759,2760,5,3137,3140,7,3458,3459,7,4153,4154,5,6432,6434,5,6978,6978,5,7675,7679,5,9723,9726,14,9823,9823,14,9919,9923,14,10035,10036,14,42736,42737,5,43596,43596,5,44200,44200,11,44648,44648,11,45096,45096,11,45544,45544,11,45992,45992,11,46440,46440,11,46888,46888,11,47336,47336,11,47784,47784,11,48232,48232,11,48680,48680,11,49128,49128,11,49576,49576,11,50024,50024,11,50472,50472,11,50920,50920,11,51368,51368,11,51816,51816,11,52264,52264,11,52712,52712,11,53160,53160,11,53608,53608,11,54056,54056,11,54504,54504,11,54952,54952,11,68108,68111,5,69933,69940,5,70197,70197,7,70498,70499,7,70845,70845,5,71229,71229,5,71727,71735,5,72154,72155,5,72344,72345,5,73023,73029,5,94095,94098,5,121403,121452,5,126981,127182,14,127538,127546,14,127990,127990,14,128391,128391,14,128445,128449,14,128500,128505,14,128752,128752,14,129160,129167,14,129356,129356,14,129432,129442,14,129648,129651,14,129751,131069,14,173,173,4,1757,1757,1,2274,2274,1,2494,2494,5,2641,2641,5,2876,2876,5,3014,3016,7,3262,3262,7,3393,3396,5,3570,3571,7,3968,3972,5,4228,4228,7,6086,6086,5,6679,6680,5,6912,6915,5,7080,7081,5,7380,7392,5,8252,8252,14,9096,9096,14,9748,9749,14,9784,9786,14,9833,9850,14,9890,9894,14,9938,9938,14,9999,9999,14,10085,10087,14,12349,12349,14,43136,43137,7,43454,43456,7,43755,43755,7,44088,44088,11,44312,44312,11,44536,44536,11,44760,44760,11,44984,44984,11,45208,45208,11,45432,45432,11,45656,45656,11,45880,45880,11,46104,46104,11,46328,46328,11,46552,46552,11,46776,46776,11,47000,47000,11,47224,47224,11,47448,47448,11,47672,47672,11,47896,47896,11,48120,48120,11,48344,48344,11,48568,48568,11,48792,48792,11,49016,49016,11,49240,49240,11,49464,49464,11,49688,49688,11,49912,49912,11,50136,50136,11,50360,50360,11,50584,50584,11,50808,50808,11,51032,51032,11,51256,51256,11,51480,51480,11,51704,51704,11,51928,51928,11,52152,52152,11,52376,52376,11,52600,52600,11,52824,52824,11,53048,53048,11,53272,53272,11,53496,53496,11,53720,53720,11,53944,53944,11,54168,54168,11,54392,54392,11,54616,54616,11,54840,54840,11,55064,55064,11,65438,65439,5,69633,69633,5,69837,69837,1,70018,70018,7,70188,70190,7,70368,70370,7,70465,70468,7,70712,70719,5,70835,70840,5,70850,70851,5,71132,71133,5,71340,71340,7,71458,71461,5,71985,71989,7,72002,72002,7,72193,72202,5,72281,72283,5,72766,72766,7,72885,72886,5,73104,73105,5,92912,92916,5,113824,113827,4,119173,119179,5,121505,121519,5,125136,125142,5,127279,127279,14,127489,127490,14,127570,127743,14,127900,127901,14,128254,128254,14,128369,128370,14,128400,128400,14,128425,128432,14,128468,128475,14,128489,128494,14,128715,128720,14,128745,128745,14,128759,128760,14,129004,129023,14,129296,129304,14,129340,129342,14,129388,129392,14,129404,129407,14,129454,129455,14,129485,129487,14,129659,129663,14,129719,129727,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2363,2363,7,2402,2403,5,2507,2508,7,2622,2624,7,2691,2691,7,2786,2787,5,2881,2884,5,3006,3006,5,3072,3072,5,3170,3171,5,3267,3268,7,3330,3331,7,3406,3406,1,3538,3540,5,3655,3662,5,3897,3897,5,4038,4038,5,4184,4185,5,4352,4447,8,6068,6069,5,6155,6157,5,6448,6449,7,6742,6742,5,6783,6783,5,6966,6970,5,7042,7042,7,7143,7143,7,7212,7219,5,7412,7412,5,8206,8207,4,8294,8303,4,8596,8601,14,9410,9410,14,9742,9742,14,9757,9757,14,9770,9770,14,9794,9794,14,9828,9828,14,9855,9855,14,9882,9882,14,9900,9903,14,9929,9933,14,9963,9967,14,9987,9988,14,10006,10006,14,10062,10062,14,10175,10175,14,11744,11775,5,42607,42607,5,43043,43044,7,43263,43263,5,43444,43445,7,43569,43570,5,43698,43700,5,43766,43766,5,44032,44032,11,44144,44144,11,44256,44256,11,44368,44368,11,44480,44480,11,44592,44592,11,44704,44704,11,44816,44816,11,44928,44928,11,45040,45040,11,45152,45152,11,45264,45264,11,45376,45376,11,45488,45488,11,45600,45600,11,45712,45712,11,45824,45824,11,45936,45936,11,46048,46048,11,46160,46160,11,46272,46272,11,46384,46384,11,46496,46496,11,46608,46608,11,46720,46720,11,46832,46832,11,46944,46944,11,47056,47056,11,47168,47168,11,47280,47280,11,47392,47392,11,47504,47504,11,47616,47616,11,47728,47728,11,47840,47840,11,47952,47952,11,48064,48064,11,48176,48176,11,48288,48288,11,48400,48400,11,48512,48512,11,48624,48624,11,48736,48736,11,48848,48848,11,48960,48960,11,49072,49072,11,49184,49184,11,49296,49296,11,49408,49408,11,49520,49520,11,49632,49632,11,49744,49744,11,49856,49856,11,49968,49968,11,50080,50080,11,50192,50192,11,50304,50304,11,50416,50416,11,50528,50528,11,50640,50640,11,50752,50752,11,50864,50864,11,50976,50976,11,51088,51088,11,51200,51200,11,51312,51312,11,51424,51424,11,51536,51536,11,51648,51648,11,51760,51760,11,51872,51872,11,51984,51984,11,52096,52096,11,52208,52208,11,52320,52320,11,52432,52432,11,52544,52544,11,52656,52656,11,52768,52768,11,52880,52880,11,52992,52992,11,53104,53104,11,53216,53216,11,53328,53328,11,53440,53440,11,53552,53552,11,53664,53664,11,53776,53776,11,53888,53888,11,54000,54000,11,54112,54112,11,54224,54224,11,54336,54336,11,54448,54448,11,54560,54560,11,54672,54672,11,54784,54784,11,54896,54896,11,55008,55008,11,55120,55120,11,64286,64286,5,66272,66272,5,68900,68903,5,69762,69762,7,69817,69818,5,69927,69931,5,70003,70003,5,70070,70078,5,70094,70094,7,70194,70195,7,70206,70206,5,70400,70401,5,70463,70463,7,70475,70477,7,70512,70516,5,70722,70724,5,70832,70832,5,70842,70842,5,70847,70848,5,71088,71089,7,71102,71102,7,71219,71226,5,71231,71232,5,71342,71343,7,71453,71455,5,71463,71467,5,71737,71738,5,71995,71996,5,72000,72000,7,72145,72147,7,72160,72160,5,72249,72249,7,72273,72278,5,72330,72342,5,72752,72758,5,72850,72871,5,72882,72883,5,73018,73018,5,73031,73031,5,73109,73109,5,73461,73462,7,94031,94031,5,94192,94193,7,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,126976,126979,14,127184,127231,14,127344,127345,14,127405,127461,14,127514,127514,14,127561,127567,14,127778,127779,14,127896,127896,14,127985,127986,14,127995,127999,5,128326,128328,14,128360,128366,14,128378,128378,14,128394,128397,14,128405,128406,14,128422,128423,14,128435,128443,14,128453,128464,14,128479,128480,14,128484,128487,14,128496,128498,14,128640,128709,14,128723,128724,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129096,129103,14,129292,129292,14,129311,129311,14,129329,129330,14,129344,129349,14,129360,129374,14,129394,129394,14,129402,129402,14,129413,129425,14,129445,129450,14,129466,129471,14,129483,129483,14,129511,129535,14,129653,129655,14,129667,129670,14,129705,129711,14,129731,129743,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2307,2307,7,2366,2368,7,2382,2383,7,2434,2435,7,2497,2500,5,2519,2519,5,2563,2563,7,2631,2632,5,2677,2677,5,2750,2752,7,2763,2764,7,2817,2817,5,2879,2879,5,2891,2892,7,2914,2915,5,3008,3008,5,3021,3021,5,3076,3076,5,3146,3149,5,3202,3203,7,3264,3265,7,3271,3272,7,3298,3299,5,3390,3390,5,3402,3404,7,3426,3427,5,3535,3535,5,3544,3550,7,3635,3635,7,3763,3763,7,3893,3893,5,3953,3966,5,3981,3991,5,4145,4145,7,4157,4158,5,4209,4212,5,4237,4237,5,4520,4607,10,5970,5971,5,6071,6077,5,6089,6099,5,6277,6278,5,6439,6440,5,6451,6456,7,6683,6683,5,6744,6750,5,6765,6770,7,6846,6846,5,6964,6964,5,6972,6972,5,7019,7027,5,7074,7077,5,7083,7085,5,7146,7148,7,7154,7155,7,7222,7223,5,7394,7400,5,7416,7417,5,8204,8204,5,8233,8233,4,8288,8292,4,8413,8416,5,8482,8482,14,8986,8987,14,9193,9203,14,9654,9654,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9775,14,9792,9792,14,9800,9811,14,9825,9826,14,9831,9831,14,9852,9853,14,9872,9873,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9936,9936,14,9941,9960,14,9974,9974,14,9982,9985,14,9992,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10145,10145,14,11013,11015,14,11503,11505,5,12334,12335,5,12951,12951,14,42612,42621,5,43014,43014,5,43047,43047,7,43204,43205,5,43335,43345,5,43395,43395,7,43450,43451,7,43561,43566,5,43573,43574,5,43644,43644,5,43710,43711,5,43758,43759,7,44005,44005,5,44012,44012,7,44060,44060,11,44116,44116,11,44172,44172,11,44228,44228,11,44284,44284,11,44340,44340,11,44396,44396,11,44452,44452,11,44508,44508,11,44564,44564,11,44620,44620,11,44676,44676,11,44732,44732,11,44788,44788,11,44844,44844,11,44900,44900,11,44956,44956,11,45012,45012,11,45068,45068,11,45124,45124,11,45180,45180,11,45236,45236,11,45292,45292,11,45348,45348,11,45404,45404,11,45460,45460,11,45516,45516,11,45572,45572,11,45628,45628,11,45684,45684,11,45740,45740,11,45796,45796,11,45852,45852,11,45908,45908,11,45964,45964,11,46020,46020,11,46076,46076,11,46132,46132,11,46188,46188,11,46244,46244,11,46300,46300,11,46356,46356,11,46412,46412,11,46468,46468,11,46524,46524,11,46580,46580,11,46636,46636,11,46692,46692,11,46748,46748,11,46804,46804,11,46860,46860,11,46916,46916,11,46972,46972,11,47028,47028,11,47084,47084,11,47140,47140,11,47196,47196,11,47252,47252,11,47308,47308,11,47364,47364,11,47420,47420,11,47476,47476,11,47532,47532,11,47588,47588,11,47644,47644,11,47700,47700,11,47756,47756,11,47812,47812,11,47868,47868,11,47924,47924,11,47980,47980,11,48036,48036,11,48092,48092,11,48148,48148,11,48204,48204,11,48260,48260,11,48316,48316,11,48372,48372,11,48428,48428,11,48484,48484,11,48540,48540,11,48596,48596,11,48652,48652,11,48708,48708,11,48764,48764,11,48820,48820,11,48876,48876,11,48932,48932,11,48988,48988,11,49044,49044,11,49100,49100,11,49156,49156,11,49212,49212,11,49268,49268,11,49324,49324,11,49380,49380,11,49436,49436,11,49492,49492,11,49548,49548,11,49604,49604,11,49660,49660,11,49716,49716,11,49772,49772,11,49828,49828,11,49884,49884,11,49940,49940,11,49996,49996,11,50052,50052,11,50108,50108,11,50164,50164,11,50220,50220,11,50276,50276,11,50332,50332,11,50388,50388,11,50444,50444,11,50500,50500,11,50556,50556,11,50612,50612,11,50668,50668,11,50724,50724,11,50780,50780,11,50836,50836,11,50892,50892,11,50948,50948,11,51004,51004,11,51060,51060,11,51116,51116,11,51172,51172,11,51228,51228,11,51284,51284,11,51340,51340,11,51396,51396,11,51452,51452,11,51508,51508,11,51564,51564,11,51620,51620,11,51676,51676,11,51732,51732,11,51788,51788,11,51844,51844,11,51900,51900,11,51956,51956,11,52012,52012,11,52068,52068,11,52124,52124,11,52180,52180,11,52236,52236,11,52292,52292,11,52348,52348,11,52404,52404,11,52460,52460,11,52516,52516,11,52572,52572,11,52628,52628,11,52684,52684,11,52740,52740,11,52796,52796,11,52852,52852,11,52908,52908,11,52964,52964,11,53020,53020,11,53076,53076,11,53132,53132,11,53188,53188,11,53244,53244,11,53300,53300,11,53356,53356,11,53412,53412,11,53468,53468,11,53524,53524,11,53580,53580,11,53636,53636,11,53692,53692,11,53748,53748,11,53804,53804,11,53860,53860,11,53916,53916,11,53972,53972,11,54028,54028,11,54084,54084,11,54140,54140,11,54196,54196,11,54252,54252,11,54308,54308,11,54364,54364,11,54420,54420,11,54476,54476,11,54532,54532,11,54588,54588,11,54644,54644,11,54700,54700,11,54756,54756,11,54812,54812,11,54868,54868,11,54924,54924,11,54980,54980,11,55036,55036,11,55092,55092,11,55148,55148,11,55216,55238,9,65056,65071,5,65529,65531,4,68097,68099,5,68159,68159,5,69446,69456,5,69688,69702,5,69808,69810,7,69815,69816,7,69821,69821,1,69888,69890,5,69932,69932,7,69957,69958,7,70016,70017,5,70067,70069,7,70079,70080,7,70089,70092,5,70095,70095,5,70191,70193,5,70196,70196,5,70198,70199,5,70367,70367,5,70371,70378,5,70402,70403,7,70462,70462,5,70464,70464,5,70471,70472,7,70487,70487,5,70502,70508,5,70709,70711,7,70720,70721,7,70725,70725,7,70750,70750,5,70833,70834,7,70841,70841,7,70843,70844,7,70846,70846,7,70849,70849,7,71087,71087,5,71090,71093,5,71100,71101,5,71103,71104,5,71216,71218,7,71227,71228,7,71230,71230,7,71339,71339,5,71341,71341,5,71344,71349,5,71351,71351,5,71456,71457,7,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123628,123631,5,125252,125258,5,126980,126980,14,127183,127183,14,127245,127247,14,127340,127343,14,127358,127359,14,127377,127386,14,127462,127487,6,127491,127503,14,127535,127535,14,127548,127551,14,127568,127569,14,127744,127777,14,127780,127891,14,127894,127895,14,127897,127899,14,127902,127984,14,127987,127989,14,127991,127994,14,128000,128253,14,128255,128317,14,128329,128334,14,128336,128359,14,128367,128368,14,128371,128377,14,128379,128390,14,128392,128393,14,128398,128399,14,128401,128404,14,128407,128419,14,128421,128421,14,128424,128424,14,128433,128434,14,128444,128444,14,128450,128452,14,128465,128467,14,128476,128478,14,128481,128481,14,128483,128483,14,128488,128488,14,128495,128495,14,128499,128499,14,128506,128591,14,128710,128714,14,128721,128722,14,128725,128725,14,128728,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129664,129666,14,129671,129679,14,129686,129704,14,129712,129718,14,129728,129730,14,129744,129750,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2259,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3134,3136,5,3142,3144,5,3157,3158,5,3201,3201,5,3260,3260,5,3263,3263,5,3266,3266,5,3270,3270,5,3274,3275,7,3285,3286,5,3328,3329,5,3387,3388,5,3391,3392,7,3398,3400,7,3405,3405,5,3415,3415,5,3457,3457,5,3530,3530,5,3536,3537,7,3542,3542,5,3551,3551,5,3633,3633,5,3636,3642,5,3761,3761,5,3764,3772,5,3864,3865,5,3895,3895,5,3902,3903,7,3967,3967,7,3974,3975,5,3993,4028,5,4141,4144,5,4146,4151,5,4155,4156,7,4182,4183,7,4190,4192,5,4226,4226,5,4229,4230,5,4253,4253,5,4448,4519,9,4957,4959,5,5938,5940,5,6002,6003,5,6070,6070,7,6078,6085,7,6087,6088,7,6109,6109,5,6158,6158,4,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6848,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7673,5,8203,8203,4,8205,8205,13,8232,8232,4,8234,8238,4,8265,8265,14,8293,8293,4,8400,8412,5,8417,8417,5,8421,8432,5,8505,8505,14,8617,8618,14,9000,9000,14,9167,9167,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9776,9783,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9935,14,9937,9937,14,9939,9940,14,9961,9962,14,9968,9973,14,9975,9978,14,9981,9981,14,9986,9986,14,9989,9989,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10084,14,10133,10135,14,10160,10160,14,10548,10549,14,11035,11036,14,11093,11093,14,11647,11647,5,12330,12333,5,12336,12336,14,12441,12442,5,12953,12953,14,42608,42610,5,42654,42655,5,43010,43010,5,43019,43019,5,43045,43046,5,43052,43052,5,43188,43203,7,43232,43249,5,43302,43309,5,43346,43347,7,43392,43394,5,43443,43443,5,43446,43449,5,43452,43453,5,43493,43493,5,43567,43568,7,43571,43572,7,43587,43587,5,43597,43597,7,43696,43696,5,43703,43704,5,43713,43713,5,43756,43757,5,43765,43765,7,44003,44004,7,44006,44007,7,44009,44010,7,44013,44013,5,44033,44059,12,44061,44087,12,44089,44115,12,44117,44143,12,44145,44171,12,44173,44199,12,44201,44227,12,44229,44255,12,44257,44283,12,44285,44311,12,44313,44339,12,44341,44367,12,44369,44395,12,44397,44423,12,44425,44451,12,44453,44479,12,44481,44507,12,44509,44535,12,44537,44563,12,44565,44591,12,44593,44619,12,44621,44647,12,44649,44675,12,44677,44703,12,44705,44731,12,44733,44759,12,44761,44787,12,44789,44815,12,44817,44843,12,44845,44871,12,44873,44899,12,44901,44927,12,44929,44955,12,44957,44983,12,44985,45011,12,45013,45039,12,45041,45067,12,45069,45095,12,45097,45123,12,45125,45151,12,45153,45179,12,45181,45207,12,45209,45235,12,45237,45263,12,45265,45291,12,45293,45319,12,45321,45347,12,45349,45375,12,45377,45403,12,45405,45431,12,45433,45459,12,45461,45487,12,45489,45515,12,45517,45543,12,45545,45571,12,45573,45599,12,45601,45627,12,45629,45655,12,45657,45683,12,45685,45711,12,45713,45739,12,45741,45767,12,45769,45795,12,45797,45823,12,45825,45851,12,45853,45879,12,45881,45907,12,45909,45935,12,45937,45963,12,45965,45991,12,45993,46019,12,46021,46047,12,46049,46075,12,46077,46103,12,46105,46131,12,46133,46159,12,46161,46187,12,46189,46215,12,46217,46243,12,46245,46271,12,46273,46299,12,46301,46327,12,46329,46355,12,46357,46383,12,46385,46411,12,46413,46439,12,46441,46467,12,46469,46495,12,46497,46523,12,46525,46551,12,46553,46579,12,46581,46607,12,46609,46635,12,46637,46663,12,46665,46691,12,46693,46719,12,46721,46747,12,46749,46775,12,46777,46803,12,46805,46831,12,46833,46859,12,46861,46887,12,46889,46915,12,46917,46943,12,46945,46971,12,46973,46999,12,47001,47027,12,47029,47055,12,47057,47083,12,47085,47111,12,47113,47139,12,47141,47167,12,47169,47195,12,47197,47223,12,47225,47251,12,47253,47279,12,47281,47307,12,47309,47335,12,47337,47363,12,47365,47391,12,47393,47419,12,47421,47447,12,47449,47475,12,47477,47503,12,47505,47531,12,47533,47559,12,47561,47587,12,47589,47615,12,47617,47643,12,47645,47671,12,47673,47699,12,47701,47727,12,47729,47755,12,47757,47783,12,47785,47811,12,47813,47839,12,47841,47867,12,47869,47895,12,47897,47923,12,47925,47951,12,47953,47979,12,47981,48007,12,48009,48035,12,48037,48063,12,48065,48091,12,48093,48119,12,48121,48147,12,48149,48175,12,48177,48203,12,48205,48231,12,48233,48259,12,48261,48287,12,48289,48315,12,48317,48343,12,48345,48371,12,48373,48399,12,48401,48427,12,48429,48455,12,48457,48483,12,48485,48511,12,48513,48539,12,48541,48567,12,48569,48595,12,48597,48623,12,48625,48651,12,48653,48679,12,48681,48707,12,48709,48735,12,48737,48763,12,48765,48791,12,48793,48819,12,48821,48847,12,48849,48875,12,48877,48903,12,48905,48931,12,48933,48959,12,48961,48987,12,48989,49015,12,49017,49043,12,49045,49071,12,49073,49099,12,49101,49127,12,49129,49155,12,49157,49183,12,49185,49211,12,49213,49239,12,49241,49267,12,49269,49295,12,49297,49323,12,49325,49351,12,49353,49379,12,49381,49407,12,49409,49435,12,49437,49463,12,49465,49491,12,49493,49519,12,49521,49547,12,49549,49575,12,49577,49603,12,49605,49631,12,49633,49659,12,49661,49687,12,49689,49715,12,49717,49743,12,49745,49771,12,49773,49799,12,49801,49827,12,49829,49855,12,49857,49883,12,49885,49911,12,49913,49939,12,49941,49967,12,49969,49995,12,49997,50023,12,50025,50051,12,50053,50079,12,50081,50107,12,50109,50135,12,50137,50163,12,50165,50191,12,50193,50219,12,50221,50247,12,50249,50275,12,50277,50303,12,50305,50331,12,50333,50359,12,50361,50387,12,50389,50415,12,50417,50443,12,50445,50471,12,50473,50499,12,50501,50527,12,50529,50555,12,50557,50583,12,50585,50611,12,50613,50639,12,50641,50667,12,50669,50695,12,50697,50723,12,50725,50751,12,50753,50779,12,50781,50807,12,50809,50835,12,50837,50863,12,50865,50891,12,50893,50919,12,50921,50947,12,50949,50975,12,50977,51003,12,51005,51031,12,51033,51059,12,51061,51087,12,51089,51115,12,51117,51143,12,51145,51171,12,51173,51199,12,51201,51227,12,51229,51255,12,51257,51283,12,51285,51311,12,51313,51339,12,51341,51367,12,51369,51395,12,51397,51423,12,51425,51451,12,51453,51479,12,51481,51507,12,51509,51535,12,51537,51563,12,51565,51591,12,51593,51619,12,51621,51647,12,51649,51675,12,51677,51703,12,51705,51731,12,51733,51759,12,51761,51787,12,51789,51815,12,51817,51843,12,51845,51871,12,51873,51899,12,51901,51927,12,51929,51955,12,51957,51983,12,51985,52011,12,52013,52039,12,52041,52067,12,52069,52095,12,52097,52123,12,52125,52151,12,52153,52179,12,52181,52207,12,52209,52235,12,52237,52263,12,52265,52291,12,52293,52319,12,52321,52347,12,52349,52375,12,52377,52403,12,52405,52431,12,52433,52459,12,52461,52487,12,52489,52515,12,52517,52543,12,52545,52571,12,52573,52599,12,52601,52627,12,52629,52655,12,52657,52683,12,52685,52711,12,52713,52739,12,52741,52767,12,52769,52795,12,52797,52823,12,52825,52851,12,52853,52879,12,52881,52907,12,52909,52935,12,52937,52963,12,52965,52991,12,52993,53019,12,53021,53047,12,53049,53075,12,53077,53103,12,53105,53131,12,53133,53159,12,53161,53187,12,53189,53215,12,53217,53243,12,53245,53271,12,53273,53299,12,53301,53327,12,53329,53355,12,53357,53383,12,53385,53411,12,53413,53439,12,53441,53467,12,53469,53495,12,53497,53523,12,53525,53551,12,53553,53579,12,53581,53607,12,53609,53635,12,53637,53663,12,53665,53691,12,53693,53719,12,53721,53747,12,53749,53775,12,53777,53803,12,53805,53831,12,53833,53859,12,53861,53887,12,53889,53915,12,53917,53943,12,53945,53971,12,53973,53999,12,54001,54027,12,54029,54055,12,54057,54083,12,54085,54111,12,54113,54139,12,54141,54167,12,54169,54195,12,54197,54223,12,54225,54251,12,54253,54279,12,54281,54307,12,54309,54335,12,54337,54363,12,54365,54391,12,54393,54419,12,54421,54447,12,54449,54475,12,54477,54503,12,54505,54531,12,54533,54559,12,54561,54587,12,54589,54615,12,54617,54643,12,54645,54671,12,54673,54699,12,54701,54727,12,54729,54755,12,54757,54783,12,54785,54811,12,54813,54839,12,54841,54867,12,54869,54895,12,54897,54923,12,54925,54951,12,54953,54979,12,54981,55007,12,55009,55035,12,55037,55063,12,55065,55091,12,55093,55119,12,55121,55147,12,55149,55175,12,55177,55203,12,55243,55291,10,65024,65039,5,65279,65279,4,65520,65528,4,66045,66045,5,66422,66426,5,68101,68102,5,68152,68154,5,68325,68326,5,69291,69292,5,69632,69632,7,69634,69634,7,69759,69761,5]'); } diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 729e83aaa7..c03f0744a5 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -108,7 +108,7 @@ export class URI implements UriComponents { && typeof (thing).path === 'string' && typeof (thing).query === 'string' && typeof (thing).scheme === 'string' - && typeof (thing).fsPath === 'function' + && typeof (thing).fsPath === 'string' && typeof (thing).with === 'function' && typeof (thing).toString === 'function'; } diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index 8b535e93c4..2c40722027 100644 --- a/src/vs/base/node/extpath.ts +++ b/src/vs/base/node/extpath.ts @@ -7,7 +7,6 @@ import * as fs from 'fs'; import { rtrim } from 'vs/base/common/strings'; import { sep, join, normalize, dirname, basename } from 'vs/base/common/path'; import { readdirSync } from 'vs/base/node/pfs'; -import { promisify } from 'util'; /** * Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 @@ -53,7 +52,7 @@ export function realcaseSync(path: string): string | null { export async function realpath(path: string): Promise { try { - return await promisify(fs.realpath)(path); + return await fs.promises.realpath(path); } catch (error) { // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization @@ -63,7 +62,7 @@ export async function realpath(path: string): Promise { // to not resolve links but to simply see if the path is read accessible or not. const normalizedPath = normalizePath(path); - await promisify(fs.access)(normalizedPath, fs.constants.R_OK); + await fs.promises.access(normalizedPath, fs.constants.R_OK); return normalizedPath; } diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index 82fdd4359a..e902368935 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; //@ts-check +'use strict'; /** * @param {NodeRequire} nodeRequire @@ -30,22 +30,6 @@ function factory(nodeRequire, path, fs, perf) { return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); }); } - /** - * @param {string} file - * @returns {Promise} - */ - function lstat(file) { - return new Promise((c, e) => fs.lstat(file, (err, stats) => err ? e(err) : c(stats))); - } - - /** - * @param {string} dir - * @returns {Promise} - */ - function readdir(dir) { - return new Promise((c, e) => fs.readdir(dir, (err, files) => err ? e(err) : c(files))); - } - /** * @param {string} dir * @returns {Promise} @@ -54,53 +38,20 @@ function factory(nodeRequire, path, fs, perf) { return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir))); } - /** - * @param {string} dir - * @returns {Promise} - */ - function rmdir(dir) { - return new Promise((c, e) => fs.rmdir(dir, err => err ? e(err) : c(undefined))); - } - - /** - * @param {string} file - * @returns {Promise} - */ - function unlink(file) { - return new Promise((c, e) => fs.unlink(file, err => err ? e(err) : c(undefined))); - } - /** * @param {string} location * @returns {Promise} */ function rimraf(location) { - return lstat(location).then(stat => { - if (stat.isDirectory() && !stat.isSymbolicLink()) { - return readdir(location) - .then(children => Promise.all(children.map(child => rimraf(path.join(location, child))))) - .then(() => rmdir(location)); - } else { - return unlink(location); - } - }, err => { - if (err.code === 'ENOENT') { - return undefined; - } - throw err; - }); + 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(function (resolve, reject) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - reject(err); - return; - } - resolve(data); - }); - }); + return new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data))); } /** @@ -109,18 +60,9 @@ function factory(nodeRequire, path, fs, perf) { * @returns {Promise} */ function writeFile(file, content) { - return new Promise(function (resolve, reject) { - fs.writeFile(file, content, 'utf8', function (err) { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); + return new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c())); } - /** * @param {string} userDataPath * @returns {object} @@ -186,10 +128,10 @@ function factory(nodeRequire, path, fs, perf) { const initialLocale = locale; - perf.mark('nlsGeneration:start'); + perf.mark('code/willGenerateNls'); const defaultResult = function (locale) { - perf.mark('nlsGeneration:end'); + perf.mark('code/didGenerateNls'); return Promise.resolve({ locale: locale, availableLanguages: {} }); }; try { @@ -240,7 +182,7 @@ function factory(nodeRequire, path, fs, perf) { if (fileExists) { // We don't wait for this. No big harm if we can't touch touch(coreLocation).catch(() => { }); - perf.mark('nlsGeneration:end'); + perf.mark('code/didGenerateNls'); return result; } return mkdirp(coreLocation).then(() => { @@ -279,7 +221,7 @@ function factory(nodeRequire, path, fs, perf) { writes.push(writeFile(translationsConfigFile, JSON.stringify(packConfig.translations))); return Promise.all(writes); }).then(() => { - perf.mark('nlsGeneration:end'); + perf.mark('code/didGenerateNls'); return result; }).catch(err => { console.error('Generating translation files failed.', err); @@ -301,8 +243,10 @@ function factory(nodeRequire, path, fs, perf) { } +// @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'); diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index b5b3f51304..0db12c429b 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -3,17 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { Queue } from 'vs/base/common/async'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as platform from 'vs/base/common/platform'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; -import { promisify } from 'util'; -import { isRootOrDriveLetter } from 'vs/base/common/extpath'; +import { isEqualOrParent, isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; +//#region Constants + // See https://github.com/microsoft/vscode/issues/30180 const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB @@ -25,6 +26,10 @@ const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE; export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE; +//#endregion + +//#region rimraf + export enum RimRafMode { /** @@ -40,12 +45,19 @@ export enum RimRafMode { MOVE } +/** + * Allows to delete the provied path (either file or folder) recursively + * with the options: + * - `UNLINK`: direct removal from disk + * - `MOVE`: faster variant that first moves the target to temp dir and then + * deletes it in the background without waiting for that to finish. + */ export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { if (isRootOrDriveLetter(path)) { throw new Error('rimraf - will refuse to recursively delete root'); } - // delete: via unlink + // delete: via rmDir if (mode === RimRafMode.UNLINK) { return rimrafUnlink(path); } @@ -54,32 +66,17 @@ export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { +async function rimrafMove(path: string): Promise { try { - const stat = await lstat(path); - - // Folder delete (recursive) - NOT for symbolic links though! - if (stat.isDirectory() && !stat.isSymbolicLink()) { - - // Children - const children = await readdir(path); - await Promise.all(children.map(child => rimrafUnlink(join(path, child)))); - - // Folder - await promisify(fs.rmdir)(path); + const pathInTemp = join(tmpdir(), generateUuid()); + try { + await fs.promises.rename(path, pathInTemp); + } catch (error) { + return rimrafUnlink(path); // if rename fails, delete without tmp dir } - // Single file delete - else { - - // chmod as needed to allow for unlink - const mode = stat.mode; - if (!(mode & 128)) { // 128 === 0200 - await chmod(path, mode | 128); - } - - return unlink(path); - } + // Delete but do not return as promise + rimrafUnlink(pathInTemp).catch(error => {/* ignore */ }); } catch (error) { if (error.code !== 'ENOENT') { throw error; @@ -87,22 +84,8 @@ async function rimrafUnlink(path: string): Promise { } } -async function rimrafMove(path: string): Promise { - try { - const pathInTemp = join(os.tmpdir(), generateUuid()); - try { - await rename(path, pathInTemp); - } catch (error) { - return rimrafUnlink(path); // if rename fails, delete without tmp dir - } - - // Delete but do not return as promise - rimrafUnlink(pathInTemp); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } +async function rimrafUnlink(path: string): Promise { + return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -110,170 +93,281 @@ export function rimrafSync(path: string): void { throw new Error('rimraf - will refuse to recursively delete root'); } + fs.rmdirSync(path, { recursive: true }); +} + +//#endregion + +//#region readdir with NFC support (macos) + +export interface IDirent { + name: string; + + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} + +/** + * Drop-in replacement of `fs.readdir` with support + * for converting from macOS NFD unicon form to NFC + * (https://github.com/nodejs/node/issues/2165) + */ +export async function readdir(path: string): Promise; +export async function readdir(path: string, options: { withFileTypes: true }): Promise; +export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); +} + +async function safeReaddirWithFileTypes(path: string): Promise { try { - const stat = fs.lstatSync(path); - - // Folder delete (recursive) - NOT for symbolic links though! - if (stat.isDirectory() && !stat.isSymbolicLink()) { - - // Children - const children = readdirSync(path); - children.map(child => rimrafSync(join(path, child))); - - // Folder - fs.rmdirSync(path); - } - - // Single file delete - else { - - // chmod as needed to allow for unlink - const mode = stat.mode; - if (!(mode & 128)) { // 128 === 0200 - fs.chmodSync(path, mode | 128); - } - - return fs.unlinkSync(path); - } + return await fs.promises.readdir(path, { withFileTypes: true }); } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } -} - -export async function readdir(path: string): Promise { - return handleDirectoryChildren(await promisify(fs.readdir)(path)); -} - -export async function readdirWithFileTypes(path: string): Promise { - const children = await promisify(fs.readdir)(path, { withFileTypes: true }); - - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - if (platform.isMacintosh) { - for (const child of children) { - child.name = normalizeNFC(child.name); - } + console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } - return children; + // Fallback to manually reading and resolving each + // children of the folder in case we hit an error + // previously. + // This can only really happen on exotic file systems + // such as explained in #115645 where we get entries + // from `readdir` that we can later not `lstat`. + const result: IDirent[] = []; + const children = await readdir(path); + for (const child of children) { + let isFile = false; + let isDirectory = false; + let isSymbolicLink = false; + + try { + const lstat = await fs.promises.lstat(join(path, child)); + + isFile = lstat.isFile(); + isDirectory = lstat.isDirectory(); + isSymbolicLink = lstat.isSymbolicLink(); + } catch (error) { + console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); + } + + result.push({ + name: child, + isFile: () => isFile, + isDirectory: () => isDirectory, + isSymbolicLink: () => isSymbolicLink + }); + } + + return result; } +/** + * Drop-in replacement of `fs.readdirSync` with support + * for converting from macOS NFD unicon form to NFC + * (https://github.com/nodejs/node/issues/2165) + */ export function readdirSync(path: string): string[] { return handleDirectoryChildren(fs.readdirSync(path)); } -function handleDirectoryChildren(children: string[]): string[] { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - if (platform.isMacintosh) { - return children.map(child => normalizeNFC(child)); - } +function handleDirectoryChildren(children: string[]): string[]; +function handleDirectoryChildren(children: IDirent[]): IDirent[]; +function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[]; +function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[] { + return children.map(child => { - return children; -} + // Mac: uses NFD unicode form on disk, but we want NFC + // See also https://github.com/nodejs/node/issues/2165 -export function exists(path: string): Promise { - return promisify(fs.exists)(path); -} - -export function chmod(path: string, mode: number): Promise { - return promisify(fs.chmod)(path, mode); -} - -export function stat(path: string): Promise { - return promisify(fs.stat)(path); -} - -export interface IStatAndLink { - - // The stats of the file. If the file is a symbolic - // link, the stats will be of that target file and - // not the link itself. - // If the file is a symbolic link pointing to a non - // existing file, the stat will be of the link and - // the `dangling` flag will indicate this. - stat: fs.Stats; - - // Will be provided if the resource is a symbolic link - // on disk. Use the `dangling` flag to find out if it - // points to a resource that does not exist on disk. - symbolicLink?: { dangling: boolean }; -} - -export async function statLink(path: string): Promise { - - // First stat the link - let lstats: fs.Stats | undefined; - try { - lstats = await lstat(path); - - // Return early if the stat is not a symbolic link at all - if (!lstats.isSymbolicLink()) { - return { stat: lstats }; - } - } catch (error) { - /* ignore - use stat() instead */ - } - - // If the stat is a symbolic link or failed to stat, use fs.stat() - // which for symbolic links will stat the target they point to - try { - const stats = await stat(path); - - return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; - } catch (error) { - - // If the link points to a non-existing file we still want - // to return it as result while setting dangling: true flag - if (error.code === 'ENOENT' && lstats) { - return { stat: lstats, symbolicLink: { dangling: true } }; + if (typeof child === 'string') { + return isMacintosh ? normalizeNFC(child) : child; } - throw error; + child.name = isMacintosh ? normalizeNFC(child.name) : child.name; + + return child; + }); +} + +/** + * A convinience method to read all children of a path that + * are directories. + */ +export async function readDirsInDir(dirPath: string): Promise { + const children = await readdir(dirPath); + const directories: string[] = []; + + for (const child of children) { + if (await SymlinkSupport.existsDirectory(join(dirPath, child))) { + directories.push(child); + } + } + + return directories; +} + +//#endregion + +//#region whenDeleted() + +/** + * A `Promise` that resolves when the provided `path` + * is deleted from disk. + */ +export function whenDeleted(path: string, intervalMs = 1000): Promise { + return new Promise(resolve => { + let running = false; + const interval = setInterval(() => { + if (!running) { + running = true; + fs.access(path, err => { + running = false; + + if (err) { + clearInterval(interval); + resolve(undefined); + } + }); + } + }, intervalMs); + }); +} + +//#endregion + +//#region Methods with symbolic links support + +export namespace SymlinkSupport { + + export interface IStats { + + // The stats of the file. If the file is a symbolic + // link, the stats will be of that target file and + // not the link itself. + // If the file is a symbolic link pointing to a non + // existing file, the stat will be of the link and + // the `dangling` flag will indicate this. + stat: fs.Stats; + + // Will be provided if the resource is a symbolic link + // on disk. Use the `dangling` flag to find out if it + // points to a resource that does not exist on disk. + symbolicLink?: { dangling: boolean }; + } + + /** + * Resolves the `fs.Stats` of the provided path. If the path is a + * symbolic link, the `fs.Stats` will be from the target it points + * to. If the target does not exist, `dangling: true` will be returned + * as `symbolicLink` value. + */ + export async function stat(path: string): Promise { + + // First stat the link + let lstats: fs.Stats | undefined; + try { + lstats = await fs.promises.lstat(path); + + // Return early if the stat is not a symbolic link at all + if (!lstats.isSymbolicLink()) { + return { stat: lstats }; + } + } catch (error) { + /* ignore - use stat() instead */ + } + + // If the stat is a symbolic link or failed to stat, use fs.stat() + // which for symbolic links will stat the target they point to + try { + const stats = await fs.promises.stat(path); + + return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; + } catch (error) { + + // If the link points to a non-existing file we still want + // to return it as result while setting dangling: true flag + if (error.code === 'ENOENT' && lstats) { + return { stat: lstats, symbolicLink: { dangling: true } }; + } + + // Windows: workaround a node.js bug where reparse points + // are not supported (https://github.com/nodejs/node/issues/36790) + if (isWindows && error.code === 'EACCES') { + try { + const stats = await fs.promises.stat(await fs.promises.readlink(path)); + + return { stat: stats, symbolicLink: { dangling: false } }; + } catch (error) { + + // If the link points to a non-existing file we still want + // to return it as result while setting dangling: true flag + if (error.code === 'ENOENT' && lstats) { + return { stat: lstats, symbolicLink: { dangling: true } }; + } + + throw error; + } + } + + throw error; + } + } + + /** + * Figures out if the `path` exists and is a file with support + * for symlinks. + * + * Note: this will return `false` for a symlink that exists on + * disk but is dangling (pointing to a non-existing path). + * + * Use `exists` if you only care about the path existing on disk + * or not without support for symbolic links. + */ + export async function existsFile(path: string): Promise { + try { + const { stat, symbolicLink } = await SymlinkSupport.stat(path); + + return stat.isFile() && symbolicLink?.dangling !== true; + } catch (error) { + // Ignore, path might not exist + } + + return false; + } + + /** + * Figures out if the `path` exists and is a directory with support for + * symlinks. + * + * Note: this will return `false` for a symlink that exists on + * disk but is dangling (pointing to a non-existing path). + * + * Use `exists` if you only care about the path existing on disk + * or not without support for symbolic links. + */ + export async function existsDirectory(path: string): Promise { + try { + const { stat, symbolicLink } = await SymlinkSupport.stat(path); + + return stat.isDirectory() && symbolicLink?.dangling !== true; + } catch (error) { + // Ignore, path might not exist + } + + return false; } } -export function lstat(path: string): Promise { - return promisify(fs.lstat)(path); -} +//#endregion -export function rename(oldPath: string, newPath: string): Promise { - return promisify(fs.rename)(oldPath, newPath); -} - -export function renameIgnoreError(oldPath: string, newPath: string): Promise { - return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve())); -} - -export function unlink(path: string): Promise { - return promisify(fs.unlink)(path); -} - -export function symlink(target: string, path: string, type?: string): Promise { - return promisify(fs.symlink)(target, path, type); -} - -export function truncate(path: string, len: number): Promise { - return promisify(fs.truncate)(path, len); -} - -export function readFile(path: string): Promise; -export function readFile(path: string, encoding: string): Promise; -export function readFile(path: string, encoding?: string): Promise { - return promisify(fs.readFile)(path, encoding); -} - -export async function mkdirp(path: string, mode?: number): Promise { - return promisify(fs.mkdir)(path, { mode, recursive: true }); -} - -// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback) -// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return. -// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly. -const writeFilePathQueues: Map> = new Map(); +//#region Write File +/** + * Same as `fs.writeFile` but with an additional call to + * `fs.fdatasync` after writing to ensure changes are + * flushed to disk. + * + * In addition, multiple writes to the same path are queued. + */ export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise; @@ -288,9 +382,14 @@ export function writeFile(path: string, data: string | Buffer | Uint8Array, opti }); } +// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback) +// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return. +// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly. +const writeFilePathQueues: Map> = new Map(); + function toQueueKey(path: string): string { let queueKey = path; - if (platform.isWindows || platform.isMacintosh) { + if (isWindows || isMacintosh) { queueKey = queueKey.toLowerCase(); // accommodate for case insensitive file systems } @@ -365,6 +464,11 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o }); } +/** + * Same as `fs.writeFileSync` but with an additional call to + * `fs.fdatasyncSync` after writing to ensure changes are + * flushed to disk. + */ export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void { const ensuredOptions = ensureWriteOptions(options); @@ -394,92 +498,57 @@ export function writeFileSync(path: string, data: string | Buffer, options?: IWr function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions { if (!options) { - return { mode: 0o666, flag: 'w' }; + return { mode: 0o666 /* default node.js mode for files */, flag: 'w' }; } return { - mode: typeof options.mode === 'number' ? options.mode : 0o666, + mode: typeof options.mode === 'number' ? options.mode : 0o666 /* default node.js mode for files */, flag: typeof options.flag === 'string' ? options.flag : 'w' }; } -export async function readDirsInDir(dirPath: string): Promise { - const children = await readdir(dirPath); - const directories: string[] = []; +//#endregion - for (const child of children) { - if (await dirExists(join(dirPath, child))) { - directories.push(child); - } - } - - return directories; -} - -export async function dirExists(path: string): Promise { - try { - const fileStat = await stat(path); - - return fileStat.isDirectory(); - } catch (error) { - return false; - } -} - -export async function fileExists(path: string): Promise { - try { - const fileStat = await stat(path); - - return fileStat.isFile(); - } catch (error) { - return false; - } -} - -export function whenDeleted(path: string): Promise { - - // Complete when wait marker file is deleted - return new Promise(resolve => { - let running = false; - const interval = setInterval(() => { - if (!running) { - running = true; - fs.exists(path, exists => { - running = false; - - if (!exists) { - clearInterval(interval); - resolve(undefined); - } - }); - } - }, 1000); - }); -} +//#region Move / Copy +/** + * A drop-in replacement for `fs.rename` that: + * - updates the `mtime` of the `source` after the operation + * - allows to move across multiple disks + */ export async function move(source: string, target: string): Promise { if (source === target) { - return Promise.resolve(); + return; // simulate node.js behaviour here and do a no-op if paths match } + // We have been updating `mtime` for move operations for files since the + // beginning for reasons that are no longer quite clear, but changing + // this could be risky as well. As such, trying to reason about it: + // It is very common as developer to have file watchers enabled that watch + // the current workspace for changes. Updating the `mtime` might make it + // easier for these watchers to recognize an actual change. Since changing + // a source code file also updates the `mtime`, moving a file should do so + // as well because conceptually it is a change of a similar category. async function updateMtime(path: string): Promise { - const stat = await lstat(path); - if (stat.isDirectory() || stat.isSymbolicLink()) { - return Promise.resolve(); // only for files - } - - const fd = await promisify(fs.open)(path, 'a'); try { - await promisify(fs.futimes)(fd, stat.atime, new Date()); - } catch (error) { - //ignore - } + const stat = await fs.promises.lstat(path); + if (stat.isDirectory() || stat.isSymbolicLink()) { + return; // only for files + } - return promisify(fs.close)(fd); + const fh = await fs.promises.open(path, 'a'); + try { + await fh.utimes(stat.atime, new Date()); + } finally { + await fh.close(); + } + } catch (error) { + // Ignore any error + } } try { - await rename(source, target); + await fs.promises.rename(source, target); await updateMtime(target); } catch (error) { @@ -492,7 +561,7 @@ export async function move(source: string, target: string): Promise { // 2.) The user tries to rename a file/folder that ends with a dot. This is not // really possible to move then, at least on UNC devices. if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) { - await copy(source, target); + await copy(source, target, { preserveSymlinks: false /* copying to another device */ }); await rimraf(source, RimRafMode.MOVE); await updateMtime(target); } else { @@ -501,59 +570,119 @@ export async function move(source: string, target: string): Promise { } } -export async function copy(source: string, target: string, copiedSourcesIn?: { [path: string]: boolean }): Promise { - const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null); +interface ICopyPayload { + readonly root: { source: string, target: string }; + readonly options: { preserveSymlinks: boolean }; + readonly handledSourcePaths: Set; +} - const fileStat = await stat(source); - if (!fileStat.isDirectory()) { - return doCopyFile(source, target, fileStat.mode & 511); +/** + * Recursively copies all of `source` to `target`. + * + * The options `preserveSymlinks` configures how symbolic + * links should be handled when encountered. Set to + * `false` to not preserve them and `true` otherwise. + */ +export async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise { + return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set() }); +} + +// When copying a file or folder, we want to preserve the mode +// it had and as such provide it when creating. However, modes +// can go beyond what we expect (see link below), so we mask it. +// (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588) +const COPY_MODE_MASK = 0o777; + +async function doCopy(source: string, target: string, payload: ICopyPayload): Promise { + + // Keep track of paths already copied to prevent + // cycles from symbolic links to cause issues + if (payload.handledSourcePaths.has(source)) { + return; + } else { + payload.handledSourcePaths.add(source); } - if (copiedSources[source]) { - return Promise.resolve(); // escape when there are cycles (can happen with symlinks) + const { stat, symbolicLink } = await SymlinkSupport.stat(source); + + // Symlink + if (symbolicLink) { + if (symbolicLink.dangling) { + return; // do not copy dangling symbolic links (https://github.com/microsoft/vscode/issues/111621) + } + + // Try to re-create the symlink unless `preserveSymlinks: false` + if (payload.options.preserveSymlinks) { + try { + return await doCopySymlink(source, target, payload); + } catch (error) { + // in any case of an error fallback to normal copy via dereferencing + console.warn('[node.js fs] copy of symlink failed: ', error); + } + } } - copiedSources[source] = true; // remember as copied + // Folder + if (stat.isDirectory()) { + return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, payload); + } + + // File or file-like + else { + return doCopyFile(source, target, stat.mode & COPY_MODE_MASK); + } +} + +async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise { // Create folder - await mkdirp(target, fileStat.mode & 511); + await fs.promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = await readdir(source); - for (let i = 0; i < files.length; i++) { - const file = files[i]; - await copy(join(source, file), join(target, file), copiedSources); + for (const file of files) { + await doCopy(join(source, file), join(target, file), payload); } } async function doCopyFile(source: string, target: string, mode: number): Promise { - return new Promise((resolve, reject) => { - const reader = fs.createReadStream(source); - const writer = fs.createWriteStream(target, { mode }); - let finished = false; - const finish = (error?: Error) => { - if (!finished) { - finished = true; + // Copy file + await fs.promises.copyFile(source, target); - // in error cases, pass to callback - if (error) { - return reject(error); - } - - // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 - fs.chmod(target, mode, error => error ? reject(error) : resolve()); - } - }; - - // handle errors properly - reader.once('error', error => finish(error)); - writer.once('error', error => finish(error)); - - // we are done (underlying fd has been closed) - writer.once('close', () => finish()); - - // start piping - reader.pipe(writer); - }); + // restore mode (https://github.com/nodejs/node/issues/1104) + await fs.promises.chmod(target, mode); } + +async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise { + + // Figure out link target + let linkTarget = await fs.promises.readlink(source); + + // Special case: the symlink points to a target that is + // actually within the path that is being copied. In that + // case we want the symlink to point to the target and + // not the source + if (isEqualOrParent(linkTarget, payload.root.source, !isLinux)) { + linkTarget = join(payload.root.target, linkTarget.substr(payload.root.source.length + 1)); + } + + // Create symlink + await fs.promises.symlink(linkTarget, target); +} + +//#endregion + +//#region Async FS Methods + +export async function exists(path: string): Promise { + try { + await fs.promises.access(path); + + return true; + } catch { + return false; + } +} + +//#endregion diff --git a/src/vs/base/node/powershell.ts b/src/vs/base/node/powershell.ts new file mode 100644 index 0000000000..13c58be2ec --- /dev/null +++ b/src/vs/base/node/powershell.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as pfs from 'vs/base/node/pfs'; +import * as os from 'os'; +import * as path from 'vs/base/common/path'; +import { env } from 'vs/base/common/process'; + +const WindowsPowerShell64BitLabel = 'Windows PowerShell'; +const WindowsPowerShell32BitLabel = 'Windows PowerShell (x86)'; + +// This is required, since parseInt("7-preview") will return 7. +const IntRegex: RegExp = /^\d+$/; + +const PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/; +const PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/; + +// The platform details descriptor for the platform we're on +const isProcess64Bit: boolean = process.arch === 'x64'; +const isOS64Bit: boolean = isProcess64Bit || os.arch() === 'x64'; + +export interface IPowerShellExeDetails { + readonly displayName: string; + readonly exePath: string; +} + +export interface IPossiblePowerShellExe extends IPowerShellExeDetails { + exists(): Promise; +} + +class PossiblePowerShellExe implements IPossiblePowerShellExe { + constructor( + public readonly exePath: string, + public readonly displayName: string, + private knownToExist?: boolean) { } + + public async exists(): Promise { + if (this.knownToExist === undefined) { + this.knownToExist = await pfs.SymlinkSupport.existsFile(this.exePath); + } + return this.knownToExist; + } +} + +function getProgramFilesPath( + { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { + + if (!useAlternateBitness) { + // Just use the native system bitness + return env.ProgramFiles || null; + } + + // We might be a 64-bit process looking for 32-bit program files + if (isProcess64Bit) { + return env['ProgramFiles(x86)'] || null; + } + + // We might be a 32-bit process looking for 64-bit program files + if (isOS64Bit) { + return env.ProgramW6432 || null; + } + + // We're a 32-bit process on 32-bit Windows, there is no other Program Files dir + return null; +} + +function getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string { + const windir: string = env.windir!; + + if (!useAlternateBitness) { + // Just use the native system bitness + return path.join(windir, 'System32'); + } + + // We might be a 64-bit process looking for 32-bit system32 + if (isProcess64Bit) { + return path.join(windir, 'SysWOW64'); + } + + // We might be a 32-bit process looking for 64-bit system32 + if (isOS64Bit) { + return path.join(windir, 'Sysnative'); + } + + // We're on a 32-bit Windows, so no alternate bitness + return path.join(windir, 'System32'); +} + +async function findPSCoreWindowsInstallation( + { useAlternateBitness = false, findPreview = false }: + { useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise { + + const programFilesPath = getProgramFilesPath({ useAlternateBitness }); + if (!programFilesPath) { + return null; + } + + const powerShellInstallBaseDir = path.join(programFilesPath, 'PowerShell'); + + // Ensure the base directory exists + if (!await pfs.SymlinkSupport.existsDirectory(powerShellInstallBaseDir)) { + return null; + } + + let highestSeenVersion: number = -1; + let pwshExePath: string | null = null; + for (const item of await pfs.readdir(powerShellInstallBaseDir)) { + + let currentVersion: number = -1; + if (findPreview) { + // We are looking for something like "7-preview" + + // Preview dirs all have dashes in them + const dashIndex = item.indexOf('-'); + if (dashIndex < 0) { + continue; + } + + // Verify that the part before the dash is an integer + // and that the part after the dash is "preview" + const intPart: string = item.substring(0, dashIndex); + if (!IntRegex.test(intPart) || item.substring(dashIndex + 1) !== 'preview') { + continue; + } + + currentVersion = parseInt(intPart, 10); + } else { + // Search for a directory like "6" or "7" + if (!IntRegex.test(item)) { + continue; + } + + currentVersion = parseInt(item, 10); + } + + // Ensure we haven't already seen a higher version + if (currentVersion <= highestSeenVersion) { + continue; + } + + // Now look for the file + const exePath = path.join(powerShellInstallBaseDir, item, 'pwsh.exe'); + if (!await pfs.SymlinkSupport.existsFile(exePath)) { + continue; + } + + pwshExePath = exePath; + highestSeenVersion = currentVersion; + } + + if (!pwshExePath) { + return null; + } + + const bitness: string = programFilesPath.includes('x86') ? ' (x86)' : ''; + const preview: string = findPreview ? ' Preview' : ''; + + return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview}${bitness}`, true); +} + +async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise { + // We can't proceed if there's no LOCALAPPDATA path + if (!env.LOCALAPPDATA) { + return null; + } + + // Find the base directory for MSIX application exe shortcuts + const msixAppDir = path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps'); + + if (!await pfs.SymlinkSupport.existsDirectory(msixAppDir)) { + return null; + } + + // Define whether we're looking for the preview or the stable + const { pwshMsixDirRegex, pwshMsixName } = findPreview + ? { pwshMsixDirRegex: PwshPreviewMsixRegex, pwshMsixName: 'PowerShell Preview (Store)' } + : { pwshMsixDirRegex: PwshMsixRegex, pwshMsixName: 'PowerShell (Store)' }; + + // We should find only one such application, so return on the first one + for (const subdir of await pfs.readdir(msixAppDir)) { + if (pwshMsixDirRegex.test(subdir)) { + const pwshMsixPath = path.join(msixAppDir, subdir, 'pwsh.exe'); + return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName); + } + } + + // If we find nothing, return null + return null; +} + +function findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe { + const dotnetGlobalToolExePath: string = path.join(os.homedir(), '.dotnet', 'tools', 'pwsh.exe'); + + return new PossiblePowerShellExe(dotnetGlobalToolExePath, '.NET Core PowerShell Global Tool'); +} + +function findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe | null { + + // x86 and ARM only have one WinPS on them + if (!isOS64Bit && useAlternateBitness) { + return null; + } + + const systemFolderPath = getSystem32Path({ useAlternateBitness }); + + const winPSPath = path.join(systemFolderPath, 'WindowsPowerShell', 'v1.0', 'powershell.exe'); + + let displayName: string; + if (isProcess64Bit) { + displayName = useAlternateBitness + ? WindowsPowerShell32BitLabel + : WindowsPowerShell64BitLabel; + } else if (isOS64Bit) { + displayName = useAlternateBitness + ? WindowsPowerShell64BitLabel + : WindowsPowerShell32BitLabel; + } else { + // NOTE: ARM Windows devices also have Windows PowerShell x86 on them. There is no + // "ARM Windows PowerShell". + displayName = WindowsPowerShell32BitLabel; + } + + return new PossiblePowerShellExe(winPSPath, displayName, true); +} + +/** + * Iterates through all the possible well-known PowerShell installations on a machine. + * Returned values may not exist, but come with an .exists property + * which will check whether the executable exists. + */ +async function* enumerateDefaultPowerShellInstallations(): AsyncIterable { + // Find PSCore stable first + let pwshExe = await findPSCoreWindowsInstallation(); + if (pwshExe) { + yield pwshExe; + } + + // Windows may have a 32-bit pwsh.exe + pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true }); + if (pwshExe) { + yield pwshExe; + } + + // Also look for the MSIX/UWP installation + pwshExe = await findPSCoreMsix(); + if (pwshExe) { + yield pwshExe; + } + + // Look for the .NET global tool + // Some older versions of PowerShell have a bug in this where startup will fail, + // but this is fixed in newer versions + pwshExe = findPSCoreDotnetGlobalTool(); + if (pwshExe) { + yield pwshExe; + } + + // Look for PSCore preview + pwshExe = await findPSCoreWindowsInstallation({ findPreview: true }); + if (pwshExe) { + yield pwshExe; + } + + // Find a preview MSIX + pwshExe = await findPSCoreMsix({ findPreview: true }); + if (pwshExe) { + yield pwshExe; + } + + // Look for pwsh-preview with the opposite bitness + pwshExe = await findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true }); + if (pwshExe) { + yield pwshExe; + } + + // Finally, get Windows PowerShell + + // Get the natural Windows PowerShell for the process bitness + pwshExe = findWinPS(); + if (pwshExe) { + yield pwshExe; + } + + // Get the alternate bitness Windows PowerShell + pwshExe = findWinPS({ useAlternateBitness: true }); + if (pwshExe) { + yield pwshExe; + } +} + +/** + * Iterates through PowerShell installations on the machine according + * to configuration passed in through the constructor. + * PowerShell items returned by this object are verified + * to exist on the filesystem. + */ +export async function* enumeratePowerShellInstallations(): AsyncIterable { + // Get the default PowerShell installations first + for await (const defaultPwsh of enumerateDefaultPowerShellInstallations()) { + if (await defaultPwsh.exists()) { + yield defaultPwsh; + } + } +} + +/** +* Returns the first available PowerShell executable found in the search order. +*/ +export async function getFirstAvailablePowerShellInstallation(): Promise { + for await (const pwsh of enumeratePowerShellInstallations()) { + return pwsh; + } + return null; +} diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 0470853ad6..a746f9e349 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -5,7 +5,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; -import { promisify } from 'util'; +import * as pfs from 'vs/base/node/pfs'; import * as cp from 'child_process'; import * as nls from 'vs/nls'; import * as Types from 'vs/base/common/types'; @@ -50,7 +50,7 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise { + return new Promise(resolve => { killProcess.once('error', (err) => { resolve({ success: false, error: err }); }); @@ -68,7 +68,7 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise { + return new Promise(resolve => { cp.execFile(cmd, [process.pid.toString()], { encoding: 'utf8', shell: true } as cp.ExecFileOptions, (err, stdout, stderr) => { if (err) { resolve({ success: false, error: err }); @@ -86,8 +86,8 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise { @@ -447,8 +447,8 @@ export namespace win32 { // to the current working directory. return path.join(cwd, command); } - if (paths === undefined && Types.isString(process.env.PATH)) { - paths = process.env.PATH.split(path.delimiter); + if (paths === undefined && Types.isString(process.env['PATH'])) { + paths = process.env['PATH'].split(path.delimiter); } // No PATH environment. Make path absolute to the cwd. if (paths === undefined || paths.length === 0) { @@ -456,8 +456,8 @@ export namespace win32 { } async function fileExists(path: string): Promise { - if (await promisify(fs.exists)(path)) { - return !((await promisify(fs.stat)(path)).isDirectory()); + if (await pfs.exists(path)) { + return !((await fs.promises.stat(path)).isDirectory()); } return false; } diff --git a/src/vs/base/node/shell.ts b/src/vs/base/node/shell.ts new file mode 100644 index 0000000000..c8398fee36 --- /dev/null +++ b/src/vs/base/node/shell.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import { getFirstAvailablePowerShellInstallation } from 'vs/base/node/powershell'; +import * as processes from 'vs/base/node/processes'; + +/** + * Gets the detected default shell for the _system_, not to be confused with VS Code's _default_ + * shell that the terminal uses by default. + * @param p The platform to detect the shell of. + */ +export async function getSystemShell(p: platform.Platform, env = process.env as platform.IProcessEnvironment): Promise { + if (p === platform.Platform.Windows) { + if (platform.isWindows) { + return getSystemShellWindows(); + } + // Don't detect Windows shell when not on Windows + return processes.getWindowsShell(env); + } + + return getSystemShellUnixLike(p, env); +} + +export function getSystemShellSync(p: platform.Platform, env = process.env as platform.IProcessEnvironment): string { + if (p === platform.Platform.Windows) { + if (platform.isWindows) { + return getSystemShellWindowsSync(env); + } + // Don't detect Windows shell when not on Windows + return processes.getWindowsShell(env); + } + + return getSystemShellUnixLike(p, env); +} + +let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null; +function getSystemShellUnixLike(p: platform.Platform, env: platform.IProcessEnvironment): string { + // Only use $SHELL for the current OS + if (platform.isLinux && p === platform.Platform.Mac || platform.isMacintosh && p === platform.Platform.Linux) { + return '/bin/bash'; + } + + if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { + let unixLikeTerminal: string; + if (platform.isWindows) { + unixLikeTerminal = '/bin/bash'; // for WSL + } else { + unixLikeTerminal = env['SHELL']; + + if (!unixLikeTerminal) { + try { + // It's possible for $SHELL to be unset, this API reads /etc/passwd. See https://github.com/github/codespaces/issues/1639 + // Node docs: "Throws a SystemError if a user has no username or homedir." + unixLikeTerminal = os.userInfo().shell; + } catch (err) { } + } + + if (!unixLikeTerminal) { + unixLikeTerminal = 'sh'; + } + + // Some systems have $SHELL set to /bin/false which breaks the terminal + if (unixLikeTerminal === '/bin/false') { + unixLikeTerminal = '/bin/bash'; + } + } + _TERMINAL_DEFAULT_SHELL_UNIX_LIKE = unixLikeTerminal; + } + return _TERMINAL_DEFAULT_SHELL_UNIX_LIKE; +} + +let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null; +async function getSystemShellWindows(): Promise { + if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) { + _TERMINAL_DEFAULT_SHELL_WINDOWS = (await getFirstAvailablePowerShellInstallation())!.exePath; + } + return _TERMINAL_DEFAULT_SHELL_WINDOWS; +} + +function getSystemShellWindowsSync(env: platform.IProcessEnvironment): string { + if (_TERMINAL_DEFAULT_SHELL_WINDOWS) { + return _TERMINAL_DEFAULT_SHELL_WINDOWS; + } + + const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10; + const is32ProcessOn64Windows = env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const powerShellPath = `${env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`; + return isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(env); +} diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 0118062740..aa2f49bcd3 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -5,10 +5,10 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import { createWriteStream, WriteStream } from 'fs'; +import { promises, createWriteStream, WriteStream } from 'fs'; import { Readable } from 'stream'; import { Sequencer, createCancelablePromise } from 'vs/base/common/async'; -import { mkdirp, rimraf } from 'vs/base/node/pfs'; +import { rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -86,7 +86,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa } }); - return Promise.resolve(mkdirp(targetDirName)).then(() => new Promise((c, e) => { + return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { if (token.isCancellationRequested) { return; } @@ -149,7 +149,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok // directory file names end with '/' if (/\/$/.test(fileName)) { const targetFileName = path.join(targetPath, fileName); - last = createCancelablePromise(token => mkdirp(targetFileName).then(() => readNextEntry(token)).then(undefined, e)); + last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); return; } diff --git a/src/vs/base/parts/ipc/browser/ipc.mp.ts b/src/vs/base/parts/ipc/browser/ipc.mp.ts new file mode 100644 index 0000000000..0ebbb9b2dc --- /dev/null +++ b/src/vs/base/parts/ipc/browser/ipc.mp.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; + +/** + * An implementation of a `IPCClient` on top of DOM `MessagePort`. + */ +export class Client extends MessagePortClient implements IDisposable { + + /** + * @param clientId a way to uniquely identify this client among + * other clients. this is important for routing because every + * client can also be a server + */ + constructor(port: MessagePort, clientId: string) { + super(port, clientId); + } +} diff --git a/src/vs/base/parts/ipc/common/ipc.electron.ts b/src/vs/base/parts/ipc/common/ipc.electron.ts index d152801fd6..93bbbffd57 100644 --- a/src/vs/base/parts/ipc/common/ipc.electron.ts +++ b/src/vs/base/parts/ipc/common/ipc.electron.ts @@ -11,6 +11,11 @@ export interface Sender { send(channel: string, msg: unknown): void; } +/** + * The Electron `Protocol` leverages Electron style IPC communication (`ipcRenderer`, `ipcMain`) + * for the implementation of the `IMessagePassingProtocol`. That style of API requires a channel + * name for sending data. + */ export class Protocol implements IMessagePassingProtocol { constructor(private sender: Sender, readonly onMessage: Event) { } @@ -23,7 +28,7 @@ export class Protocol implements IMessagePassingProtocol { } } - dispose(): void { + disconnect(): void { this.sender.send('vscode:disconnect', null); } } diff --git a/src/vs/base/parts/ipc/common/ipc.mp.ts b/src/vs/base/parts/ipc/common/ipc.mp.ts new file mode 100644 index 0000000000..53bc48070e --- /dev/null +++ b/src/vs/base/parts/ipc/common/ipc.mp.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { VSBuffer } from 'vs/base/common/buffer'; + +/** + * Declare minimal `MessageEvent` and `MessagePort` interfaces here + * so that this utility can be used both from `browser` and + * `electron-main` namespace where message ports are available. + */ + +export interface MessageEvent { + + /** + * For our use we only consider `Uint8Array` a valid data transfer + * via message ports because our protocol implementation is buffer based. + */ + data: Uint8Array; +} + +export interface MessagePort { + + addEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void; + removeEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void; + + postMessage(message: Uint8Array): void; + + start(): void; + close(): void; +} + +/** + * The MessagePort `Protocol` leverages MessagePort style IPC communication + * for the implementation of the `IMessagePassingProtocol`. That style of API + * is a simple `onmessage` / `postMessage` pattern. + */ +export class Protocol implements IMessagePassingProtocol { + + readonly onMessage = Event.fromDOMEventEmitter(this.port, 'message', (e: MessageEvent) => VSBuffer.wrap(e.data)); + + constructor(private port: MessagePort) { + + // we must call start() to ensure messages are flowing + port.start(); + } + + send(message: VSBuffer): void { + this.port.postMessage(message.buffer); + } + + disconnect(): void { + this.port.close(); + } +} + +/** + * An implementation of a `IPCClient` on top of MessagePort style IPC communication. + */ +export class Client extends IPCClient implements IDisposable { + + private protocol: Protocol; + + constructor(port: MessagePort, clientId: string) { + const protocol = new Protocol(port); + super(protocol, clientId); + + this.protocol = protocol; + } + + dispose(): void { + this.protocol.disconnect(); + } +} diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index a656f16a00..9904ee2030 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -365,8 +365,8 @@ export class Protocol extends Disposable implements IMessagePassingProtocol { private readonly _onMessage = new Emitter(); readonly onMessage: Event = this._onMessage.event; - private readonly _onClose = new Emitter(); - readonly onClose: Event = this._onClose.event; + private readonly _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; constructor(socket: ISocket) { super(); @@ -380,7 +380,7 @@ export class Protocol extends Disposable implements IMessagePassingProtocol { } })); - this._register(this._socket.onClose(() => this._onClose.fire())); + this._register(this._socket.onClose(() => this._onDidDispose.fire())); } drain(): Promise { @@ -406,7 +406,7 @@ export class Client extends IPCClient { return new Client(new Protocol(socket), id); } - get onClose(): Event { return this.protocol.onClose; } + get onDidDispose(): Event { return this.protocol.onDidDispose; } constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) { super(protocol, id, ipcLogger); @@ -621,8 +621,8 @@ export class PersistentProtocol implements IMessagePassingProtocol { private readonly _onMessage = new BufferedEmitter(); readonly onMessage: Event = this._onMessage.event; - private readonly _onClose = new BufferedEmitter(); - readonly onClose: Event = this._onClose.event; + private readonly _onDidDispose = new BufferedEmitter(); + readonly onDidDispose: Event = this._onDidDispose.event; private readonly _onSocketClose = new BufferedEmitter(); readonly onSocketClose: Event = this._onSocketClose.event; @@ -747,6 +747,10 @@ export class PersistentProtocol implements IMessagePassingProtocol { return this._socket; } + public getMillisSinceLastIncomingData(): number { + return Date.now() - this._socketReader.lastReadTime; + } + public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void { this._isReconnecting = true; @@ -783,7 +787,7 @@ export class PersistentProtocol implements IMessagePassingProtocol { } public acceptDisconnect(): void { - this._onClose.fire(); + this._onDidDispose.fire(); } private _receiveMessage(msg: ProtocolMessage): void { @@ -820,7 +824,7 @@ export class PersistentProtocol implements IMessagePassingProtocol { } else if (msg.type === ProtocolMessageType.Control) { this._onControlMessage.fire(msg.data); } else if (msg.type === ProtocolMessageType.Disconnect) { - this._onClose.fire(); + this._onDidDispose.fire(); } else if (msg.type === ProtocolMessageType.ReplayRequest) { // Send again all unacknowledged messages const toSend = this._outgoingUnackMsg.toArray(); diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index b697372096..8041636420 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -505,6 +505,7 @@ export interface IIPCLogger { export class ChannelClient implements IChannelClient, IDisposable { + private isDisposed: boolean = false; private state: State = State.Uninitialized; private activeRequests = new Set(); private handlers = new Map(); @@ -525,9 +526,15 @@ export class ChannelClient implements IChannelClient, IDisposable { return { call(command: string, arg?: any, cancellationToken?: CancellationToken) { + if (that.isDisposed) { + return Promise.reject(errors.canceled()); + } return that.requestPromise(channelName, command, arg, cancellationToken); }, listen(event: string, arg: any) { + if (that.isDisposed) { + return Promise.reject(errors.canceled()); + } return that.requestEvent(channelName, event, arg); } } as T; @@ -725,6 +732,7 @@ export class ChannelClient implements IChannelClient, IDisposable { } dispose(): void { + this.isDisposed = true; if (this.protocolListener) { this.protocolListener.dispose(); this.protocolListener = null; diff --git a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts b/src/vs/base/parts/ipc/electron-main/ipc.electron.ts similarity index 90% rename from src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts rename to src/vs/base/parts/ipc/electron-main/ipc.electron.ts index 0ebedc47bf..29ecf305e3 100644 --- a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts +++ b/src/vs/base/parts/ipc/electron-main/ipc.electron.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ipcMain, WebContents } from 'electron'; import { Event, Emitter } from 'vs/base/common/event'; import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc'; -import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron'; -import { ipcMain, WebContents } from 'electron'; +import { Protocol as ElectronProtocol } from 'vs/base/parts/ipc/common/ipc.electron'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -22,6 +22,9 @@ function createScopedOnMessageEvent(senderId: number, eventName: string): Event< return Event.map(onMessageFromSender, ({ message }) => message ? VSBuffer.wrap(message) : message as null); } +/** + * An implemention of `IPCServer` on top of Electron `ipcMain` API. + */ export class Server extends IPCServer { private static readonly Clients = new Map(); @@ -42,7 +45,7 @@ export class Server extends IPCServer { const onMessage = createScopedOnMessageEvent(id, 'vscode:message') as Event; const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'vscode:disconnect')), onDidClientReconnect.event); - const protocol = new Protocol(webContents, onMessage); + const protocol = new ElectronProtocol(webContents, onMessage); return { protocol, onDidClientDisconnect }; }); diff --git a/src/vs/base/parts/ipc/electron-main/ipc.mp.ts b/src/vs/base/parts/ipc/electron-main/ipc.mp.ts new file mode 100644 index 0000000000..753dddc5ca --- /dev/null +++ b/src/vs/base/parts/ipc/electron-main/ipc.mp.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BrowserWindow, ipcMain, IpcMainEvent, MessagePortMain } from 'electron'; +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; + +/** + * An implementation of a `IPCClient` on top of Electron `MessagePortMain`. + */ +export class Client extends MessagePortClient implements IDisposable { + + /** + * @param clientId a way to uniquely identify this client among + * other clients. this is important for routing because every + * client can also be a server + */ + constructor(port: MessagePortMain, clientId: string) { + super({ + addEventListener: (type, listener) => port.addListener(type, listener), + removeEventListener: (type, listener) => port.removeListener(type, listener), + postMessage: message => port.postMessage(message), + start: () => port.start(), + close: () => port.close() + }, clientId); + } +} + +/** + * This method opens a message channel connection + * in the target window. The target window needs + * to use the `Server` from `electron-sandbox/ipc.mp`. + */ +export async function connect(window: BrowserWindow): Promise { + + // Assert healthy window to talk to + if (window.isDestroyed() || window.webContents.isDestroyed()) { + throw new Error('ipc.mp#connect: Cannot talk to window because it is closed or destroyed'); + } + + // Ask to create message channel inside the window + // and send over a UUID to correlate the response + const nonce = generateUuid(); + window.webContents.send('vscode:createMessageChannel', nonce); + + // Wait until the window has returned the `MessagePort` + // We need to filter by the `nonce` to ensure we listen + // to the right response. + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePortMain }>(ipcMain, 'vscode:createMessageChannelResult', (e: IpcMainEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); + + return port; +} diff --git a/src/vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox.ts b/src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts similarity index 71% rename from src/vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox.ts rename to src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts index ac38fa4af7..0b3960609e 100644 --- a/src/vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox.ts +++ b/src/vs/base/parts/ipc/electron-sandbox/ipc.electron.ts @@ -5,28 +5,34 @@ import { Event } from 'vs/base/common/event'; import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; -import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron'; +import { Protocol as ElectronProtocol } from 'vs/base/parts/ipc/common/ipc.electron'; import { IDisposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +/** + * An implemention of `IPCClient` on top of Electron `ipcRenderer` IPC communication + * provided from sandbox globals (via preload script). + */ export class Client extends IPCClient implements IDisposable { - private protocol: Protocol; + private protocol: ElectronProtocol; - private static createProtocol(): Protocol { + private static createProtocol(): ElectronProtocol { const onMessage = Event.fromNodeEventEmitter(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message)); ipcRenderer.send('vscode:hello'); - return new Protocol(ipcRenderer, onMessage); + + return new ElectronProtocol(ipcRenderer, onMessage); } constructor(id: string) { const protocol = Client.createProtocol(); super(protocol, id); + this.protocol = protocol; } dispose(): void { - this.protocol.dispose(); + this.protocol.disconnect(); } } diff --git a/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts b/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts new file mode 100644 index 0000000000..3518356ae2 --- /dev/null +++ b/src/vs/base/parts/ipc/electron-sandbox/ipc.mp.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { Event } from 'vs/base/common/event'; +import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc'; +import { Protocol as MessagePortProtocol } from 'vs/base/parts/ipc/common/ipc.mp'; + +/** + * An implementation of a `IPCServer` on top of MessagePort style IPC communication. + * The clients register themselves via Electron IPC transfer. + */ +export class Server extends IPCServer { + + private static getOnDidClientConnect(): Event { + + // Clients connect via `vscode:createMessageChannel` to get a + // `MessagePort` that is ready to be used. For every connection + // we create a pair of message ports and send it back. + // + // The `nonce` is included so that the main side has a chance to + // correlate the response back to the sender. + const onCreateMessageChannel = Event.fromNodeEventEmitter(ipcRenderer, 'vscode:createMessageChannel', (_, nonce: string) => nonce); + + return Event.map(onCreateMessageChannel, nonce => { + + // Create a new pair of ports and protocol for this connection + const { port1: incomingPort, port2: outgoingPort } = new MessageChannel(); + const protocol = new MessagePortProtocol(incomingPort); + + const result: ClientConnectionEvent = { + protocol, + // Not part of the standard spec, but in Electron we get a `close` event + // when the other side closes. We can use this to detect disconnects + // (https://github.com/electron/electron/blob/11-x-y/docs/api/message-port-main.md#event-close) + onDidClientDisconnect: Event.fromDOMEventEmitter(incomingPort, 'close') + }; + + // Send one port back to the requestor + ipcRenderer.postMessage('vscode:createMessageChannelResult', nonce, [outgoingPort]); + + return result; + }); + } + + constructor() { + super(Server.getOnDidClientConnect()); + } +} diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 74da6e5562..729fd84e51 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -5,6 +5,7 @@ import { createHash } from 'crypto'; import { Socket, Server as NetServer, createConnection, createServer } from 'net'; +import * as zlib from 'zlib'; import { Event, Emitter } from 'vs/base/common/event'; import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { join } from 'vs/base/common/path'; @@ -120,24 +121,130 @@ const enum ReadState { export class WebSocketNodeSocket extends Disposable implements ISocket { public readonly socket: NodeSocket; + public readonly permessageDeflate: boolean; + private _totalIncomingWireBytes: number; + private _totalIncomingDataBytes: number; + private _totalOutgoingWireBytes: number; + private _totalOutgoingDataBytes: number; + private readonly _zlibInflate: zlib.InflateRaw | null; + private readonly _zlibDeflate: zlib.DeflateRaw | null; + private _zlibDeflateFlushWaitingCount: number; + private readonly _onDidZlibFlush = this._register(new Emitter()); + private readonly _recordInflateBytes: boolean; + private readonly _recordedInflateBytes: Buffer[] = []; + private readonly _pendingInflateData: Buffer[] = []; + private readonly _pendingDeflateData: Buffer[] = []; private readonly _incomingData: ChunkStream; private readonly _onData = this._register(new Emitter()); + private readonly _onClose = this._register(new Emitter()); + private _isEnded: boolean = false; private readonly _state = { state: ReadState.PeekHeader, readLen: Constants.MinHeaderByteSize, + fin: 0, mask: 0 }; - constructor(socket: NodeSocket) { + public get totalIncomingWireBytes(): number { + return this._totalIncomingWireBytes; + } + + public get totalIncomingDataBytes(): number { + return this._totalIncomingDataBytes; + } + + public get totalOutgoingWireBytes(): number { + return this._totalOutgoingWireBytes; + } + + public get totalOutgoingDataBytes(): number { + return this._totalOutgoingDataBytes; + } + + public get recordedInflateBytes(): VSBuffer { + if (this._recordInflateBytes) { + return VSBuffer.wrap(Buffer.concat(this._recordedInflateBytes)); + } + return VSBuffer.alloc(0); + } + + /** + * Create a socket which can communicate using WebSocket frames. + * + * **NOTE**: When using the permessage-deflate WebSocket extension, if parts of inflating was done + * in a different zlib instance, we need to pass all those bytes into zlib, otherwise the inflate + * might hit an inflated portion referencing a distance too far back. + * + * @param socket The underlying socket + * @param permessageDeflate Use the permessage-deflate WebSocket extension + * @param inflateBytes "Seed" zlib inflate with these bytes. + * @param recordInflateBytes Record all bytes sent to inflate + */ + constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean) { super(); this.socket = socket; + this._totalIncomingWireBytes = 0; + this._totalIncomingDataBytes = 0; + this._totalOutgoingWireBytes = 0; + this._totalOutgoingDataBytes = 0; + this.permessageDeflate = permessageDeflate; + this._recordInflateBytes = recordInflateBytes; + if (permessageDeflate) { + // See https://tools.ietf.org/html/rfc7692#page-16 + // To simplify our logic, we don't negociate the window size + // and simply dedicate (2^15) / 32kb per web socket + this._zlibInflate = zlib.createInflateRaw({ + windowBits: 15 + }); + this._zlibInflate.on('error', (err) => { + // zlib errors are fatal, since we have no idea how to recover + console.error(err); + onUnexpectedError(err); + this._onClose.fire(); + }); + this._zlibInflate.on('data', (data: Buffer) => { + this._pendingInflateData.push(data); + }); + if (inflateBytes) { + this._zlibInflate.write(inflateBytes.buffer); + this._zlibInflate.flush(() => { + this._pendingInflateData.length = 0; + }); + } + + this._zlibDeflate = zlib.createDeflateRaw({ + windowBits: 15 + }); + this._zlibDeflate.on('error', (err) => { + // zlib errors are fatal, since we have no idea how to recover + console.error(err); + onUnexpectedError(err); + this._onClose.fire(); + }); + this._zlibDeflate.on('data', (data: Buffer) => { + this._pendingDeflateData.push(data); + }); + } else { + this._zlibInflate = null; + this._zlibDeflate = null; + } + this._zlibDeflateFlushWaitingCount = 0; this._incomingData = new ChunkStream(); this._register(this.socket.onData(data => this._acceptChunk(data))); + this._register(this.socket.onClose(() => this._onClose.fire())); } public dispose(): void { - this.socket.dispose(); + if (this._zlibDeflateFlushWaitingCount > 0) { + // Wait for any outstanding writes to finish before disposing + this._register(this._onDidZlibFlush.event(() => { + this.dispose(); + })); + } else { + this.socket.dispose(); + super.dispose(); + } } public onData(listener: (e: VSBuffer) => void): IDisposable { @@ -145,7 +252,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { } public onClose(listener: () => void): IDisposable { - return this.socket.onClose(listener); + return this._onClose.event(listener); } public onEnd(listener: () => void): IDisposable { @@ -153,6 +260,36 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { } public write(buffer: VSBuffer): void { + this._totalOutgoingDataBytes += buffer.byteLength; + + if (this._zlibDeflate) { + this._zlibDeflate.write(buffer.buffer); + + this._zlibDeflateFlushWaitingCount++; + // See https://zlib.net/manual.html#Constants + this._zlibDeflate.flush(/*Z_SYNC_FLUSH*/2, () => { + this._zlibDeflateFlushWaitingCount--; + let data = Buffer.concat(this._pendingDeflateData); + this._pendingDeflateData.length = 0; + + // See https://tools.ietf.org/html/rfc7692#section-7.2.1 + data = data.slice(0, data.length - 4); + + if (!this._isEnded) { + // Avoid ERR_STREAM_WRITE_AFTER_END + this._write(VSBuffer.wrap(data), true); + } + + if (this._zlibDeflateFlushWaitingCount === 0) { + this._onDidZlibFlush.fire(); + } + }); + } else { + this._write(buffer, false); + } + } + + private _write(buffer: VSBuffer, compressed: boolean): void { let headerLen = Constants.MinHeaderByteSize; if (buffer.byteLength < 126) { headerLen += 0; @@ -163,7 +300,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { } const header = VSBuffer.alloc(headerLen); - header.writeUInt8(0b10000010, 0); + if (compressed) { + // The RSV1 bit indicates a compressed frame + header.writeUInt8(0b11000010, 0); + } else { + header.writeUInt8(0b10000010, 0); + } if (buffer.byteLength < 126) { header.writeUInt8(buffer.byteLength, 1); } else if (buffer.byteLength < 2 ** 16) { @@ -184,10 +326,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset); } + this._totalOutgoingWireBytes += header.byteLength + buffer.byteLength; this.socket.write(VSBuffer.concat([header, buffer])); } public end(): void { + this._isEnded = true; this.socket.end(); } @@ -195,6 +339,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { if (data.byteLength === 0) { return; } + this._totalIncomingWireBytes += data.byteLength; this._incomingData.acceptChunk(data); @@ -203,14 +348,15 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { if (this._state.state === ReadState.PeekHeader) { // peek to see if we can read the entire header const peekHeader = this._incomingData.peek(this._state.readLen); - // const firstByte = peekHeader.readUInt8(0); - // const finBit = (firstByte & 0b10000000) >>> 7; + const firstByte = peekHeader.readUInt8(0); + const finBit = (firstByte & 0b10000000) >>> 7; const secondByte = peekHeader.readUInt8(1); const hasMask = (secondByte & 0b10000000) >>> 7; const len = (secondByte & 0b01111111); this._state.state = ReadState.ReadHeader; this._state.readLen = Constants.MinHeaderByteSize + (hasMask ? 4 : 0) + (len === 126 ? 2 : 0) + (len === 127 ? 8 : 0); + this._state.fin = finBit; this._state.mask = 0; } else if (this._state.state === ReadState.ReadHeader) { @@ -263,13 +409,37 @@ export class WebSocketNodeSocket extends Disposable implements ISocket { this._state.readLen = Constants.MinHeaderByteSize; this._state.mask = 0; - this._onData.fire(body); + if (this._zlibInflate) { + // See https://tools.ietf.org/html/rfc7692#section-7.2.2 + if (this._recordInflateBytes) { + this._recordedInflateBytes.push(Buffer.from(body.buffer)); + } + this._zlibInflate.write(body.buffer); + if (this._state.fin) { + if (this._recordInflateBytes) { + this._recordedInflateBytes.push(Buffer.from([0x00, 0x00, 0xff, 0xff])); + } + this._zlibInflate.write(Buffer.from([0x00, 0x00, 0xff, 0xff])); + } + this._zlibInflate.flush(() => { + const data = Buffer.concat(this._pendingInflateData); + this._pendingInflateData.length = 0; + this._totalIncomingDataBytes += data.length; + this._onData.fire(VSBuffer.wrap(data)); + }); + } else { + this._totalIncomingDataBytes += body.byteLength; + this._onData.fire(body); + } } } } - public drain(): Promise { - return this.socket.drain(); + public async drain(): Promise { + if (this._zlibDeflateFlushWaitingCount > 0) { + await Event.toPromise(this._onDidZlibFlush.event); + } + await this.socket.drain(); } } diff --git a/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts b/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts new file mode 100644 index 0000000000..ce88a6e754 --- /dev/null +++ b/src/vs/base/parts/ipc/test/browser/ipc.mp.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Event } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp'; + +suite('IPC, MessagePorts', () => { + + test('message passing', async () => { + const { port1, port2 } = new MessageChannel(); + + const client1 = new MessagePortClient(port1, 'client1'); + const client2 = new MessagePortClient(port2, 'client2'); + + client1.registerChannel('client1', { + call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise { + switch (command) { + case 'testMethodClient1': return Promise.resolve('success1'); + default: return Promise.reject(new Error('not implemented')); + } + }, + + listen(_: unknown, event: string, arg?: any): Event { + switch (event) { + default: throw new Error('not implemented'); + } + } + }); + + client2.registerChannel('client2', { + call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise { + switch (command) { + case 'testMethodClient2': return Promise.resolve('success2'); + default: return Promise.reject(new Error('not implemented')); + } + }, + + listen(_: unknown, event: string, arg?: any): Event { + switch (event) { + default: throw new Error('not implemented'); + } + } + }); + + const channelClient1 = client2.getChannel('client1'); + assert.strictEqual(await channelClient1.call('testMethodClient1'), 'success1'); + + const channelClient2 = client1.getChannel('client2'); + assert.strictEqual(await channelClient2.call('testMethodClient2'), 'success2'); + + client1.dispose(); + client2.dispose(); + }); +}); diff --git a/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts b/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts new file mode 100644 index 0000000000..cfb3a53931 --- /dev/null +++ b/src/vs/base/parts/ipc/test/electron-sandbox/ipc.mp.test.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp'; + +suite('IPC, MessagePorts', () => { + + test('message port close event', async () => { + const { port1, port2 } = new MessageChannel(); + + const client1 = new MessagePortClient(port1, 'client1'); + const client2 = new MessagePortClient(port2, 'client2'); + + // This test ensures that Electron's API for the close event + // does not break because we rely on it to dispose client + // connections from the server. + // + // This event is not provided by browser MessagePort API though. + const whenClosed = new Promise(resolve => port1.addEventListener('close', () => resolve(true))); + + client2.dispose(); + + assert.ok(await whenClosed); + + client1.dispose(); + }); +}); diff --git a/src/vs/base/parts/ipc/test/node/ipc.cp.test.ts b/src/vs/base/parts/ipc/test/node/ipc.cp.test.ts index bcec128f06..7ed1f6d736 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.cp.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.cp.test.ts @@ -11,7 +11,7 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; function createClient(): Client { return new Client(getPathFromAmdModule(require, 'bootstrap-fork'), { serverName: 'TestServer', - env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true } + env: { VSCODE_AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true } }); } diff --git a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts index 0f3a641e90..61d42fb379 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts @@ -135,13 +135,13 @@ suite('IPC, Socket Protocol', () => { a.send(VSBuffer.fromString('foobarfarboo')); const msg1 = await bMessages.waitForOne(); - assert.equal(msg1.toString(), 'foobarfarboo'); + assert.strictEqual(msg1.toString(), 'foobarfarboo'); const buffer = VSBuffer.alloc(1); buffer.writeUInt8(123, 0); a.send(buffer); const msg2 = await bMessages.waitForOne(); - assert.equal(msg2.readUInt8(0), 123); + assert.strictEqual(msg2.readUInt8(0), 123); }); @@ -160,7 +160,7 @@ suite('IPC, Socket Protocol', () => { a.send(VSBuffer.fromString(JSON.stringify(data))); const msg = await bMessages.waitForOne(); - assert.deepEqual(JSON.parse(msg.toString()), data); + assert.deepStrictEqual(JSON.parse(msg.toString()), data); }); }); @@ -179,49 +179,49 @@ suite('PersistentProtocol reconnection', () => { const bMessages = new MessageStream(b); a.send(VSBuffer.fromString('a1')); - assert.equal(a.unacknowledgedCount, 1); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(a.unacknowledgedCount, 1); + assert.strictEqual(b.unacknowledgedCount, 0); a.send(VSBuffer.fromString('a2')); - assert.equal(a.unacknowledgedCount, 2); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(a.unacknowledgedCount, 2); + assert.strictEqual(b.unacknowledgedCount, 0); a.send(VSBuffer.fromString('a3')); - assert.equal(a.unacknowledgedCount, 3); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(a.unacknowledgedCount, 3); + assert.strictEqual(b.unacknowledgedCount, 0); const a1 = await bMessages.waitForOne(); - assert.equal(a1.toString(), 'a1'); - assert.equal(a.unacknowledgedCount, 3); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(a1.toString(), 'a1'); + assert.strictEqual(a.unacknowledgedCount, 3); + assert.strictEqual(b.unacknowledgedCount, 0); const a2 = await bMessages.waitForOne(); - assert.equal(a2.toString(), 'a2'); - assert.equal(a.unacknowledgedCount, 3); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(a2.toString(), 'a2'); + assert.strictEqual(a.unacknowledgedCount, 3); + assert.strictEqual(b.unacknowledgedCount, 0); const a3 = await bMessages.waitForOne(); - assert.equal(a3.toString(), 'a3'); - assert.equal(a.unacknowledgedCount, 3); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(a3.toString(), 'a3'); + assert.strictEqual(a.unacknowledgedCount, 3); + assert.strictEqual(b.unacknowledgedCount, 0); b.send(VSBuffer.fromString('b1')); - assert.equal(a.unacknowledgedCount, 3); - assert.equal(b.unacknowledgedCount, 1); + assert.strictEqual(a.unacknowledgedCount, 3); + assert.strictEqual(b.unacknowledgedCount, 1); const b1 = await aMessages.waitForOne(); - assert.equal(b1.toString(), 'b1'); - assert.equal(a.unacknowledgedCount, 0); - assert.equal(b.unacknowledgedCount, 1); + assert.strictEqual(b1.toString(), 'b1'); + assert.strictEqual(a.unacknowledgedCount, 0); + assert.strictEqual(b.unacknowledgedCount, 1); a.send(VSBuffer.fromString('a4')); - assert.equal(a.unacknowledgedCount, 1); - assert.equal(b.unacknowledgedCount, 1); + assert.strictEqual(a.unacknowledgedCount, 1); + assert.strictEqual(b.unacknowledgedCount, 1); const b2 = await bMessages.waitForOne(); - assert.equal(b2.toString(), 'a4'); - assert.equal(a.unacknowledgedCount, 1); - assert.equal(b.unacknowledgedCount, 0); + assert.strictEqual(b2.toString(), 'a4'); + assert.strictEqual(a.unacknowledgedCount, 1); + assert.strictEqual(b.unacknowledgedCount, 0); }); }); diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index bc5ba1dd70..6c59345140 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -121,7 +121,7 @@ font-size: 11px; padding: 0 6px; display: flex; - height: 100%; + height: 27.5px; align-items: center; } diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 3659fc0189..1b56f535c9 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -30,7 +30,7 @@ import { Color } from 'vs/base/common/color'; import { registerCodicon, Codicon } from 'vs/base/common/codicons'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { escape } from 'vs/base/common/strings'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; export interface IQuickInputOptions { idPrefix: string; @@ -72,7 +72,7 @@ const $ = dom.$; type Writeable = { -readonly [P in keyof T]: T[P] }; -const backButtonIcon = registerCodicon('quick-input-back', Codicon.arrowLeft, localize('backButtonIcon', 'Icon for the back button in the quick input dialog.')); +const backButtonIcon = registerCodicon('quick-input-back', Codicon.arrowLeft); const backButton = { iconClass: backButtonIcon.classNames, @@ -967,7 +967,7 @@ class QuickPick extends QuickInput implements IQuickPi const validationMessage = this.validationMessage || ''; if (this._lastValidationMessage !== validationMessage) { this._lastValidationMessage = validationMessage; - dom.reset(this.ui.message, ...renderCodicons(escape(validationMessage))); + dom.reset(this.ui.message, ...renderLabelWithIcons(escape(validationMessage))); this.showMessageDecoration(this.validationMessage ? Severity.Error : Severity.Ignore); } this.ui.customButton.label = this.customLabel || ''; @@ -1103,7 +1103,7 @@ class InputBox extends QuickInput implements IInputBox { const validationMessage = this.validationMessage || this.noValidationMessage; if (this._lastValidationMessage !== validationMessage) { this._lastValidationMessage = validationMessage; - dom.reset(this.ui.message, ...renderCodicons(validationMessage)); + dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage)); this.showMessageDecoration(this.validationMessage ? Severity.Error : Severity.Ignore); } } diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 28af119b85..1f871aa649 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput'; import { IMatch } from 'vs/base/common/filters'; -import { matchesFuzzyCodiconAware, parseCodicons } from 'vs/base/common/codicon'; +import { matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; import { compareAnything } from 'vs/base/common/comparers'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -34,6 +34,7 @@ interface IListElement { readonly index: number; readonly item: IQuickPickItem; readonly saneLabel: string; + readonly saneMeta?: string; readonly saneAriaLabel: string; readonly saneDescription?: string; readonly saneDetail?: string; @@ -49,6 +50,7 @@ class ListElement implements IListElement, IDisposable { index!: number; item!: IQuickPickItem; saneLabel!: string; + saneMeta!: string; saneAriaLabel!: string; saneDescription?: string; saneDetail?: string; @@ -127,7 +129,7 @@ class ListElementRenderer implements IListRenderer { + const action = new Action(`id-${index}`, '', cssClasses, true, async () => { element.fireButtonTriggered({ button, item: element.item }); - return Promise.resolve(); }); action.tooltip = button.tooltip || ''; return action; @@ -248,6 +249,7 @@ export class QuickInputList { matchOnDescription = false; matchOnDetail = false; matchOnLabel = true; + matchOnMeta = true; sortByLabel = true; private readonly _onChangedAllVisibleChecked = new Emitter(); onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; @@ -421,10 +423,11 @@ export class QuickInputList { if (item.type !== 'separator') { const previous = index && inputElements[index - 1]; const saneLabel = item.label && item.label.replace(/\r?\n/g, ' '); + const saneMeta = item.meta && item.meta.replace(/\r?\n/g, ' '); const saneDescription = item.description && item.description.replace(/\r?\n/g, ' '); const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' '); const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail] - .map(s => s && parseCodicons(s).text) + .map(s => s && parseLabelWithIcons(s).text) .filter(s => !!s) .join(', '); @@ -432,6 +435,7 @@ export class QuickInputList { index, item, saneLabel, + saneMeta, saneAriaLabel, saneDescription, saneDetail, @@ -597,14 +601,15 @@ export class QuickInputList { }); } - // Filter by value (since we support codicons, use codicon aware fuzzy matching) + // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) else { this.elements.forEach(element => { - const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneLabel))) : undefined; - const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDescription || ''))) : undefined; - const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDetail || ''))) : undefined; + const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined; + const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || ''))) : undefined; + const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || ''))) : undefined; + const metaHighlights = this.matchOnMeta ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneMeta || ''))) : undefined; - if (labelHighlights || descriptionHighlights || detailHighlights) { + if (labelHighlights || descriptionHighlights || detailHighlights || metaHighlights) { element.labelHighlights = labelHighlights; element.descriptionHighlights = descriptionHighlights; element.detailHighlights = detailHighlights; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index da56944504..aec8398629 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -21,6 +21,7 @@ export interface IQuickPickItem { type?: 'item'; id?: string; label: string; + meta?: string; ariaLabel?: string; description?: string; detail?: string; diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index 41ef7f0394..484219dc15 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -7,7 +7,7 @@ // ####################################################################### // ### ### // ### electron.d.ts types we need in a common layer for reuse ### -// ### (copied from Electron 9.x) ### +// ### (copied from Electron 11.x) ### // ### ### // ####################################################################### @@ -212,7 +212,7 @@ export interface SaveDialogReturnValue { export interface FileFilter { - // Docs: http://electronjs.org/docs/api/structures/file-filter + // Docs: https://electronjs.org/docs/api/structures/file-filter extensions: string[]; name: string; @@ -220,7 +220,7 @@ export interface FileFilter { export interface InputEvent { - // Docs: http://electronjs.org/docs/api/structures/input-event + // Docs: https://electronjs.org/docs/api/structures/input-event /** * An array of modifiers of the event, can be `shift`, `control`, `ctrl`, `alt`, @@ -232,7 +232,7 @@ export interface InputEvent { export interface MouseInputEvent extends InputEvent { - // Docs: http://electronjs.org/docs/api/structures/mouse-input-event + // Docs: https://electronjs.org/docs/api/structures/mouse-input-event /** * The button pressed, can be `left`, `middle`, `right`. diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index 3bf38e44ec..d329c5fd92 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -35,6 +35,17 @@ } }, + /** + * @param {string} channel + * @param {any} message + * @param {MessagePort[]} transfer + */ + postMessage(channel, message, transfer) { + if (validateIPC(channel)) { + ipcRenderer.postMessage(channel, message, transfer); + } + }, + /** * @param {string} channel * @param {any[]} args @@ -114,6 +125,7 @@ */ process: { get platform() { return process.platform; }, + get arch() { return process.arch; }, get env() { return process.env; }, get versions() { return process.versions; }, get type() { return 'renderer'; }, diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index 1f3a54f5d1..9c8ac36e99 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -7,35 +7,54 @@ // ####################################################################### // ### ### // ### electron.d.ts types we expose from electron-sandbox ### -// ### (copied from Electron 9.x) ### +// ### (copied from Electron 11.x) ### // ### ### // ####################################################################### +export interface IpcRendererEvent extends Event { + + // Docs: https://electronjs.org/docs/api/structures/ipc-renderer-event + + /** + * A list of MessagePorts that were transferred with this message + */ + ports: MessagePort[]; + /** + * The `IpcRenderer` instance that emitted the event originally + */ + sender: IpcRenderer; + /** + * The `webContents.id` that sent the message, you can call + * `event.sender.sendTo(event.senderId, ...)` to reply to the message, see + * ipcRenderer.sendTo for more information. This only applies to messages sent from + * a different renderer. Messages sent directly from the main process set + * `event.senderId` to `0`. + */ + senderId: number; +} export interface IpcRenderer { /** * Listens to `channel`, when a new message arrives `listener` would be called with * `listener(event, args...)`. */ - on(channel: string, listener: (event: unknown, ...args: any[]) => void): void; - + on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; /** * Adds a one time `listener` function for the event. This `listener` is invoked * only the next time a message is sent to `channel`, after which it is removed. */ - once(channel: string, listener: (event: unknown, ...args: any[]) => void): void; - + once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; /** * Removes the specified `listener` from the listener array for the specified * `channel`. */ - removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void; - + removeListener(channel: string, listener: (...args: any[]) => void): this; /** * Send an asynchronous message to the main process via `channel`, along with * arguments. Arguments will be serialized with the Structured Clone Algorithm, - * just like `postMessage`, so prototype chains will not be included. Sending - * Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception. + * just like `window.postMessage`, so prototype chains will not be included. + * Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an + * exception. * * > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special * Electron objects is deprecated, and will begin throwing an exception starting @@ -43,8 +62,51 @@ export interface IpcRenderer { * * The main process handles it by listening for `channel` with the `ipcMain` * module. + * + * If you need to transfer a `MessagePort` to the main process, use + * `ipcRenderer.postMessage`. + * + * If you want to receive a single response from the main process, like the result + * of a method call, consider using `ipcRenderer.invoke`. */ send(channel: string, ...args: any[]): void; + /** + * Resolves with the response from the main process. + * + * Send a message to the main process via `channel` and expect a result + * asynchronously. Arguments will be serialized with the Structured Clone + * Algorithm, just like `window.postMessage`, so prototype chains will not be + * included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw + * an exception. + * + * > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special + * Electron objects is deprecated, and will begin throwing an exception starting + * with Electron 9. + * + * The main process should listen for `channel` with `ipcMain.handle()`. + * + * For example: + * + * If you need to transfer a `MessagePort` to the main process, use + * `ipcRenderer.postMessage`. + * + * If you do not need a response to the message, consider using `ipcRenderer.send`. + */ + invoke(channel: string, ...args: any[]): Promise; + /** + * Send a message to the main process, optionally transferring ownership of zero or + * more `MessagePort` objects. + * + * The transferred `MessagePort` objects will be available in the main process as + * `MessagePortMain` objects by accessing the `ports` property of the emitted + * event. + * + * For example: + * + * For more information on using `MessagePort` and `MessageChannel`, see the MDN + * documentation. + */ + postMessage(channel: string, message: any, transfer?: MessagePort[]): void; } export interface WebFrame { @@ -70,16 +132,23 @@ export interface CrashReporter { * with crashes that occur in other renderer processes or in the main process. * * **Note:** Parameters have limits on the length of the keys and values. Key names - * must be no longer than 39 bytes, and values must be no longer than 127 bytes. + * must be no longer than 39 bytes, and values must be no longer than 20320 bytes. * Keys with names longer than the maximum will be silently ignored. Key values * longer than the maximum length will be truncated. + * + * **Note:** On linux values that are longer than 127 bytes will be chunked into + * multiple keys, each 127 bytes in length. E.g. `addExtraParameter('foo', + * 'a'.repeat(130))` will result in two chunked keys `foo__1` and `foo__2`, the + * first will contain the first 127 bytes and the second will contain the remaining + * 3 bytes. On your crash reporting backend you should stitch together keys in + * this format. */ addExtraParameter(key: string, value: string): void; } export interface ProcessMemoryInfo { - // Docs: http://electronjs.org/docs/api/structures/process-memory-info + // Docs: https://electronjs.org/docs/api/structures/process-memory-info /** * The amount of memory not shared by other processes, such as JS heap or HTML @@ -133,10 +202,7 @@ export interface CrashReporterStartOptions { rateLimit?: boolean; /** * If true, crash reports will be compressed and uploaded with `Content-Encoding: - * gzip`. Not all collection servers support compressed payloads. Default is - * `false`. - * - * @platform darwin,win32 + * gzip`. Default is `false`. */ compress?: boolean; /** diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 8d0e374c1f..fdbcf28b3f 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -3,10 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { globals, INodeProcess, IProcessEnvironment } from 'vs/base/common/platform'; +import { globals, IProcessEnvironment } from 'vs/base/common/platform'; import { ProcessMemoryInfo, CrashReporter, IpcRenderer, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; -export interface ISandboxNodeProcess extends INodeProcess { +/** + * In sandboxed renderers we cannot expose all of the `process` global of node.js + */ +export interface IPartialNodeProcess { /** * The process.platform property returns a string identifying the operating system platform @@ -14,6 +17,12 @@ export interface ISandboxNodeProcess extends INodeProcess { */ readonly platform: 'win32' | 'linux' | 'darwin'; + /** + * The process.arch property returns a string identifying the CPU architecture + * on which the Node.js process is running. + */ + readonly arch: string; + /** * The type will always be Electron renderer. */ @@ -34,24 +43,6 @@ export interface ISandboxNodeProcess extends INodeProcess { */ readonly execPath: string; - /** - * Resolve the true process environment to use and apply it to `process.env`. - * - * There are different layers of environment that will apply: - * - `process.env`: this is the actual environment of the process before this method - * - `shellEnv` : if the program was not started from a terminal, we resolve all shell - * variables to get the same experience as if the program was started from - * a terminal (Linux, macOS) - * - `userEnv` : this is instance specific environment, e.g. if the user started the program - * from a terminal and changed certain variables - * - * The order of overwrites is `process.env` < `shellEnv` < `userEnv`. - * - * It is critical that every process awaits this method early on startup to get the right - * set of environment in `process.env`. - */ - resolveEnv(userEnv: IProcessEnvironment): Promise; - /** * A listener on the process. Only a small subset of listener types are allowed. */ @@ -73,10 +64,32 @@ export interface ISandboxNodeProcess extends INodeProcess { getProcessMemoryInfo: () => Promise; } +export interface ISandboxNodeProcess extends IPartialNodeProcess { + + /** + * A custom method we add to `process`: Resolve the true process environment to use and + * apply it to `process.env`. + * + * There are different layers of environment that will apply: + * - `process.env`: this is the actual environment of the process before this method + * - `shellEnv` : if the program was not started from a terminal, we resolve all shell + * variables to get the same experience as if the program was started from + * a terminal (Linux, macOS) + * - `userEnv` : this is instance specific environment, e.g. if the user started the program + * from a terminal and changed certain variables + * + * The order of overwrites is `process.env` < `shellEnv` < `userEnv`. + * + * It is critical that every process awaits this method early on startup to get the right + * set of environment in `process.env`. + */ + resolveEnv(userEnv: IProcessEnvironment): Promise; +} + export interface ISandboxContext { /** - * Wether the renderer runs with `sandbox` enabled or not. + * Whether the renderer runs with `sandbox` enabled or not. */ sandbox: boolean; } diff --git a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts index 3259e66e8c..3178cc8a7d 100644 --- a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts +++ b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ipcRenderer, crashReporter, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer, crashReporter, webFrame, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; suite('Sandbox', () => { test('globals', () => { - assert.ok(ipcRenderer); - assert.ok(crashReporter); - assert.ok(webFrame); + assert.ok(typeof ipcRenderer.invoke === 'function'); + assert.ok(typeof crashReporter.addExtraParameter === 'function'); + assert.ok(typeof webFrame.setZoomLevel === 'function'); + assert.ok(typeof process.platform === 'string'); }); }); diff --git a/src/vs/base/parts/storage/common/storage.ts b/src/vs/base/parts/storage/common/storage.ts index 5043b0b807..d163cf5f7f 100644 --- a/src/vs/base/parts/storage/common/storage.ts +++ b/src/vs/base/parts/storage/common/storage.ts @@ -200,9 +200,9 @@ export class Storage extends Disposable implements IStorage { return parseInt(value, 10); } - set(key: string, value: string | boolean | number | null | undefined): Promise { + async set(key: string, value: string | boolean | number | null | undefined): Promise { if (this.state === StorageState.Closed) { - return Promise.resolve(); // Return early if we are already closed + return; // Return early if we are already closed } // We remove the key for undefined/null values @@ -216,7 +216,7 @@ export class Storage extends Disposable implements IStorage { // Return early if value already set const currentValue = this.cache.get(key); if (currentValue === valueStr) { - return Promise.resolve(); + return; } // Update in cache and pending @@ -231,15 +231,15 @@ export class Storage extends Disposable implements IStorage { return this.flushDelayer.trigger(() => this.flushPending()); } - delete(key: string): Promise { + async delete(key: string): Promise { if (this.state === StorageState.Closed) { - return Promise.resolve(); // Return early if we are already closed + return; // Return early if we are already closed } // Remove from cache and add to pending const wasDeleted = this.cache.delete(key); if (!wasDeleted) { - return Promise.resolve(); // Return early if value already deleted + return; // Return early if value already deleted } if (!this.pendingDeletes.has(key)) { @@ -257,7 +257,7 @@ export class Storage extends Disposable implements IStorage { async close(): Promise { if (this.state === StorageState.Closed) { - return Promise.resolve(); // return if already closed + return; // return if already closed } // Update state @@ -282,9 +282,9 @@ export class Storage extends Disposable implements IStorage { return this.pendingInserts.size > 0 || this.pendingDeletes.size > 0; } - private flushPending(): Promise { + private async flushPending(): Promise { if (!this.hasPending) { - return Promise.resolve(); // return early if nothing to do + return; // return early if nothing to do } // Get pending data @@ -305,9 +305,9 @@ export class Storage extends Disposable implements IStorage { }); } - whenFlushed(): Promise { + async whenFlushed(): Promise { if (!this.hasPending) { - return Promise.resolve(); // return early if nothing to do + return; // return early if nothing to do } return new Promise(resolve => this.whenFlushedCallbacks.push(resolve)); diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 423915ce26..5443934e85 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import type { Database, Statement } from 'vscode-sqlite3'; +import { promises } from 'fs'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; import { basename } from 'vs/base/common/path'; -import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs'; +import { copy } from 'vs/base/node/pfs'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; interface IDatabaseConnection { @@ -186,7 +187,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // Delete the existing DB. If the path does not exist or fails to // be deleted, we do not try to recover anymore because we assume // that the path is no longer writeable for us. - return unlink(this.path).then(() => { + return promises.unlink(this.path).then(() => { // Re-open the DB fresh return this.doConnect(this.path).then(recoveryConnection => { @@ -216,7 +217,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { private backup(): Promise { const backupPath = this.toBackupPath(this.path); - return copy(this.path, backupPath); + return copy(this.path, backupPath, { preserveSymlinks: false }); } private toBackupPath(path: string): string { @@ -272,8 +273,12 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // folder is really not writeable for us. // try { - await unlink(path); - await renameIgnoreError(this.toBackupPath(path), path); + await promises.unlink(path); + try { + await promises.rename(this.toBackupPath(path), path); + } catch (error) { + // ignore + } return await this.doConnect(path); } catch (error) { @@ -412,11 +417,17 @@ export class SQLiteStorageDatabase implements IStorageDatabase { } class SQLiteStorageDatabaseLogger { + + // to reduce lots of output, require an environment variable to enable tracing + // this helps when running with --verbose normally where the storage tracing + // might hide useful output to look at + static readonly VSCODE_TRACE_STORAGE = 'VSCODE_TRACE_STORAGE'; + private readonly logTrace: ((msg: string) => void) | undefined; private readonly logError: ((error: string | Error) => void) | undefined; constructor(options?: ISQLiteStorageDatabaseLoggingOptions) { - if (options && typeof options.logTrace === 'function') { + if (options && typeof options.logTrace === 'function' && process.env[SQLiteStorageDatabaseLogger.VSCODE_TRACE_STORAGE]) { this.logTrace = options.logTrace; } diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 80adcf92b6..ccbe843917 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -5,40 +5,40 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/parts/storage/node/storage'; import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { equal, ok } from 'assert'; -import { mkdirp, writeFile, exists, unlink, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { promises } from 'fs'; +import { strictEqual, ok } from 'assert'; +import { writeFile, exists, rimraf } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { generateUuid } from 'vs/base/common/uuid'; -suite('Storage Library', function () { +flakySuite('Storage Library', function () { - // Given issues such as https://github.com/microsoft/vscode/issues/108113 - // we see random test failures when accessing the native file system. - this.retries(3); - this.timeout(1000 * 20); + let testDir: string; - function uniqueStorageDir(): string { - const id = generateUuid(); + setup(function () { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return join(tmpdir(), 'vsctests', id, 'storage2', id); - } + return promises.mkdir(testDir, { recursive: true }); + }); + + teardown(function () { + return rimraf(testDir); + }); test('basics', async () => { - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - const storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + const storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); await storage.init(); // Empty fallbacks - equal(storage.get('foo', 'bar'), 'bar'); - equal(storage.getNumber('foo', 55), 55); - equal(storage.getBoolean('foo', true), true); + strictEqual(storage.get('foo', 'bar'), 'bar'); + strictEqual(storage.getNumber('foo', 55), 55); + strictEqual(storage.getBoolean('foo', true), true); let changes = new Set(); storage.onDidChangeStorage(key => { @@ -55,19 +55,19 @@ suite('Storage Library', function () { let flushPromiseResolved = false; storage.whenFlushed().then(() => flushPromiseResolved = true); - equal(storage.get('bar'), 'foo'); - equal(storage.getNumber('barNumber'), 55); - equal(storage.getBoolean('barBoolean'), true); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.getNumber('barNumber'), 55); + strictEqual(storage.getBoolean('barBoolean'), true); - equal(changes.size, 3); + strictEqual(changes.size, 3); ok(changes.has('bar')); ok(changes.has('barNumber')); ok(changes.has('barBoolean')); let setPromiseResolved = false; await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true); - equal(setPromiseResolved, true); - equal(flushPromiseResolved, true); + strictEqual(setPromiseResolved, true); + strictEqual(flushPromiseResolved, true); changes = new Set(); @@ -75,7 +75,7 @@ suite('Storage Library', function () { storage.set('bar', 'foo'); storage.set('barNumber', 55); storage.set('barBoolean', true); - equal(changes.size, 0); + strictEqual(changes.size, 0); // Simple deletes const delete1Promise = storage.delete('bar'); @@ -86,7 +86,7 @@ suite('Storage Library', function () { ok(!storage.getNumber('barNumber')); ok(!storage.getBoolean('barBoolean')); - equal(changes.size, 3); + strictEqual(changes.size, 3); ok(changes.has('bar')); ok(changes.has('barNumber')); ok(changes.has('barBoolean')); @@ -97,19 +97,16 @@ suite('Storage Library', function () { storage.delete('bar'); storage.delete('barNumber'); storage.delete('barBoolean'); - equal(changes.size, 0); + strictEqual(changes.size, 0); let deletePromiseResolved = false; await Promise.all([delete1Promise, delete2Promise, delete3Promise]).then(() => deletePromiseResolved = true); - equal(deletePromiseResolved, true); + strictEqual(deletePromiseResolved, true); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); test('external changes', async () => { - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); class TestSQLiteStorageDatabase extends SQLiteStorageDatabase { private readonly _onDidChangeItemsExternal = new Emitter(); @@ -120,7 +117,7 @@ suite('Storage Library', function () { } } - const database = new TestSQLiteStorageDatabase(join(storageDir, 'storage.db')); + const database = new TestSQLiteStorageDatabase(join(testDir, 'storage.db')); const storage = new Storage(database); let changes = new Set(); @@ -138,35 +135,31 @@ suite('Storage Library', function () { const changed = new Map(); changed.set('foo', 'bar'); database.fireDidChangeItemsExternal({ changed }); - equal(changes.size, 0); + strictEqual(changes.size, 0); // Change is accepted if valid changed.set('foo', 'bar1'); database.fireDidChangeItemsExternal({ changed }); ok(changes.has('foo')); - equal(storage.get('foo'), 'bar1'); + strictEqual(storage.get('foo'), 'bar1'); changes.clear(); // Delete is accepted const deleted = new Set(['foo']); database.fireDidChangeItemsExternal({ deleted }); ok(changes.has('foo')); - equal(storage.get('foo', undefined), undefined); + strictEqual(storage.get('foo', undefined), undefined); changes.clear(); // Nothing happens if changing to same value database.fireDidChangeItemsExternal({ deleted }); - equal(changes.size, 0); + strictEqual(changes.size, 0); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); test('close flushes data', async () => { - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - let storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); await storage.init(); const set1Promise = storage.set('foo', 'bar'); @@ -175,26 +168,26 @@ suite('Storage Library', function () { let flushPromiseResolved = false; storage.whenFlushed().then(() => flushPromiseResolved = true); - equal(storage.get('foo'), 'bar'); - equal(storage.get('bar'), 'foo'); + strictEqual(storage.get('foo'), 'bar'); + strictEqual(storage.get('bar'), 'foo'); let setPromiseResolved = false; Promise.all([set1Promise, set2Promise]).then(() => setPromiseResolved = true); await storage.close(); - equal(setPromiseResolved, true); - equal(flushPromiseResolved, true); + strictEqual(setPromiseResolved, true); + strictEqual(flushPromiseResolved, true); - storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); await storage.init(); - equal(storage.get('foo'), 'bar'); - equal(storage.get('bar'), 'foo'); + strictEqual(storage.get('foo'), 'bar'); + strictEqual(storage.get('bar'), 'foo'); await storage.close(); - storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); await storage.init(); const delete1Promise = storage.delete('foo'); @@ -208,23 +201,19 @@ suite('Storage Library', function () { await storage.close(); - equal(deletePromiseResolved, true); + strictEqual(deletePromiseResolved, true); - storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db'))); await storage.init(); ok(!storage.get('foo')); ok(!storage.get('bar')); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); test.skip('conflicting updates', async () => { // {{SQL CARBON EDIT}} test is disabled due to failures - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - let storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + let storage = new Storage(new SQLiteStorageDatabase(join('storageDir', 'storage.db'))); await storage.init(); let changes = new Set(); @@ -239,8 +228,8 @@ suite('Storage Library', function () { let flushPromiseResolved = false; storage.whenFlushed().then(() => flushPromiseResolved = true); - equal(storage.get('foo'), 'bar3'); - equal(changes.size, 1); + strictEqual(storage.get('foo'), 'bar3'); + strictEqual(changes.size, 1); ok(changes.has('foo')); let setPromiseResolved = false; @@ -255,7 +244,7 @@ suite('Storage Library', function () { ok(!storage.get('bar')); - equal(changes.size, 1); + strictEqual(changes.size, 1); ok(changes.has('bar')); let setAndDeletePromiseResolved = false; @@ -263,14 +252,10 @@ suite('Storage Library', function () { ok(setAndDeletePromiseResolved); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); test.skip('corrupt DB recovers', async () => { // {{SQL CARBON EDIT}} test is disabled due to failures - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - const storageFile = join(storageDir, 'storage.db'); + const storageFile = join(testDir, 'storage.db'); let storage = new Storage(new SQLiteStorageDatabase(storageFile)); await storage.init(); @@ -281,34 +266,22 @@ suite('Storage Library', function () { await storage.set('foo', 'bar'); - equal(storage.get('bar'), 'foo'); - equal(storage.get('foo'), 'bar'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('foo'), 'bar'); await storage.close(); storage = new Storage(new SQLiteStorageDatabase(storageFile)); await storage.init(); - equal(storage.get('bar'), 'foo'); - equal(storage.get('foo'), 'bar'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('foo'), 'bar'); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); }); -suite('SQLite Storage Library', function () { - - // Given issues such as https://github.com/microsoft/vscode/issues/108113 - // we see random test failures when accessing the native file system. - this.retries(3); - this.timeout(1000 * 20); - - function uniqueStorageDir(): string { - const id = generateUuid(); - - return join(tmpdir(), 'vsctests', id, 'storage', id); - } +flakySuite('SQLite Storage Library', function () { function toSet(elements: string[]): Set { const set = new Set(); @@ -317,6 +290,18 @@ suite('SQLite Storage Library', function () { return set; } + let testdir: string; + + setup(function () { + testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); + + return promises.mkdir(testdir, { recursive: true }); + }); + + teardown(function () { + return rimraf(testdir); + }); + async function testDBBasics(path: string, logError?: (error: Error | string) => void) { let options!: ISQLiteStorageDatabaseOptions; if (logError) { @@ -335,49 +320,49 @@ suite('SQLite Storage Library', function () { items.set(JSON.stringify({ foo: 'bar' }), JSON.stringify({ bar: 'foo' })); let storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await storage.updateItems({ insert: items }); storedItems = await storage.getItems(); - equal(storedItems.size, items.size); - equal(storedItems.get('foo'), 'bar'); - equal(storedItems.get('some/foo/path'), 'some/bar/path'); - equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); + strictEqual(storedItems.size, items.size); + strictEqual(storedItems.get('foo'), 'bar'); + strictEqual(storedItems.get('some/foo/path'), 'some/bar/path'); + strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); await storage.updateItems({ delete: toSet(['foo']) }); storedItems = await storage.getItems(); - equal(storedItems.size, items.size - 1); + strictEqual(storedItems.size, items.size - 1); ok(!storedItems.has('foo')); - equal(storedItems.get('some/foo/path'), 'some/bar/path'); - equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); + strictEqual(storedItems.get('some/foo/path'), 'some/bar/path'); + strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); await storage.updateItems({ insert: items }); storedItems = await storage.getItems(); - equal(storedItems.size, items.size); - equal(storedItems.get('foo'), 'bar'); - equal(storedItems.get('some/foo/path'), 'some/bar/path'); - equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); + strictEqual(storedItems.size, items.size); + strictEqual(storedItems.get('foo'), 'bar'); + strictEqual(storedItems.get('some/foo/path'), 'some/bar/path'); + strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); const itemsChange = new Map(); itemsChange.set('foo', 'otherbar'); await storage.updateItems({ insert: itemsChange }); storedItems = await storage.getItems(); - equal(storedItems.get('foo'), 'otherbar'); + strictEqual(storedItems.get('foo'), 'otherbar'); await storage.updateItems({ delete: toSet(['foo', 'bar', 'some/foo/path', JSON.stringify({ foo: 'bar' })]) }); storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await storage.updateItems({ insert: items, delete: toSet(['foo', 'some/foo/path', 'other']) }); storedItems = await storage.getItems(); - equal(storedItems.size, 1); - equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); + strictEqual(storedItems.size, 1); + strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); await storage.updateItems({ delete: toSet([JSON.stringify({ foo: 'bar' })]) }); storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); let recoveryCalled = false; await storage.close(() => { @@ -386,36 +371,20 @@ suite('SQLite Storage Library', function () { return new Map(); }); - equal(recoveryCalled, false); + strictEqual(recoveryCalled, false); } test('basics', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - await testDBBasics(join(storageDir, 'storage.db')); - - await rimraf(storageDir, RimRafMode.MOVE); + await testDBBasics(join(testdir, 'storage.db')); }); test('basics (open multiple times)', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - await testDBBasics(join(storageDir, 'storage.db')); - await testDBBasics(join(storageDir, 'storage.db')); - - await rimraf(storageDir, RimRafMode.MOVE); + await testDBBasics(join(testdir, 'storage.db')); + await testDBBasics(join(testdir, 'storage.db')); }); test('basics (corrupt DB falls back to empty DB)', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - const corruptDBPath = join(storageDir, 'broken.db'); + const corruptDBPath = join(testdir, 'broken.db'); await writeFile(corruptDBPath, 'This is a broken DB'); let expectedError: any; @@ -424,16 +393,10 @@ suite('SQLite Storage Library', function () { }); ok(expectedError); - - await rimraf(storageDir, RimRafMode.MOVE); }); test('basics (corrupt DB restores from previous backup)', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - const storagePath = join(storageDir, 'storage.db'); + const storagePath = join(testdir, 'storage.db'); let storage = new SQLiteStorageDatabase(storagePath); const items = new Map(); @@ -449,10 +412,10 @@ suite('SQLite Storage Library', function () { storage = new SQLiteStorageDatabase(storagePath); const storedItems = await storage.getItems(); - equal(storedItems.size, items.size); - equal(storedItems.get('foo'), 'bar'); - equal(storedItems.get('some/foo/path'), 'some/bar/path'); - equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); + strictEqual(storedItems.size, items.size); + strictEqual(storedItems.get('foo'), 'bar'); + strictEqual(storedItems.get('some/foo/path'), 'some/bar/path'); + strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); let recoveryCalled = false; await storage.close(() => { @@ -461,17 +424,11 @@ suite('SQLite Storage Library', function () { return new Map(); }); - equal(recoveryCalled, false); - - await rimraf(storageDir, RimRafMode.MOVE); + strictEqual(recoveryCalled, false); }); test('basics (corrupt DB falls back to empty DB if backup is corrupt)', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - const storagePath = join(storageDir, 'storage.db'); + const storagePath = join(testdir, 'storage.db'); let storage = new SQLiteStorageDatabase(storagePath); const items = new Map(); @@ -488,25 +445,13 @@ suite('SQLite Storage Library', function () { storage = new SQLiteStorageDatabase(storagePath); const storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await testDBBasics(storagePath); - - await rimraf(storageDir, RimRafMode.MOVE); }); - test('basics (DB that becomes corrupt during runtime stores all state from cache on close)', async () => { - if (isWindows) { - await Promise.resolve(); // Windows will fail to write to open DB due to locking - - return; - } - - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - const storagePath = join(storageDir, 'storage.db'); + (isWindows ? test.skip /* Windows will fail to write to open DB due to locking */ : test)('basics (DB that becomes corrupt during runtime stores all state from cache on close)', async () => { + const storagePath = join(testdir, 'storage.db'); let storage = new SQLiteStorageDatabase(storagePath); const items = new Map(); @@ -518,7 +463,7 @@ suite('SQLite Storage Library', function () { await storage.close(); const backupPath = `${storagePath}.backup`; - equal(await exists(backupPath), true); + strictEqual(await exists(backupPath), true); storage = new SQLiteStorageDatabase(storagePath); await storage.getItems(); @@ -531,7 +476,7 @@ suite('SQLite Storage Library', function () { // on shutdown. await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */); - await unlink(backupPath); // also test that the recovery DB is backed up properly + await promises.unlink(backupPath); // also test that the recovery DB is backed up properly let recoveryCalled = false; await storage.close(() => { @@ -540,16 +485,16 @@ suite('SQLite Storage Library', function () { return items; }); - equal(recoveryCalled, true); - equal(await exists(backupPath), true); + strictEqual(recoveryCalled, true); + strictEqual(await exists(backupPath), true); storage = new SQLiteStorageDatabase(storagePath); const storedItems = await storage.getItems(); - equal(storedItems.size, items.size); - equal(storedItems.get('foo'), 'bar'); - equal(storedItems.get('some/foo/path'), 'some/bar/path'); - equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); + strictEqual(storedItems.size, items.size); + strictEqual(storedItems.get('foo'), 'bar'); + strictEqual(storedItems.get('some/foo/path'), 'some/bar/path'); + strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' })); recoveryCalled = false; await storage.close(() => { @@ -558,17 +503,11 @@ suite('SQLite Storage Library', function () { return new Map(); }); - equal(recoveryCalled, false); - - await rimraf(storageDir, RimRafMode.MOVE); + strictEqual(recoveryCalled, false); }); test('real world example', async function () { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - let storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db')); + let storage = new SQLiteStorageDatabase(join(testdir, 'storage.db')); const items1 = new Map(); items1.set('colorthemedata', '{"id":"vs vscode-theme-defaults-themes-light_plus-json","label":"Light+ (default light)","settingsId":"Default Light+","selector":"vs.vscode-theme-defaults-themes-light_plus-json","themeTokenColors":[{"settings":{"foreground":"#000000ff","background":"#ffffffff"}},{"scope":["meta.embedded","source.groovy.embedded"],"settings":{"foreground":"#000000ff"}},{"scope":"emphasis","settings":{"fontStyle":"italic"}},{"scope":"strong","settings":{"fontStyle":"bold"}},{"scope":"meta.diff.header","settings":{"foreground":"#000080"}},{"scope":"comment","settings":{"foreground":"#008000"}},{"scope":"constant.language","settings":{"foreground":"#0000ff"}},{"scope":["constant.numeric"],"settings":{"foreground":"#098658"}},{"scope":"constant.regexp","settings":{"foreground":"#811f3f"}},{"name":"css tags in selectors, xml tags","scope":"entity.name.tag","settings":{"foreground":"#800000"}},{"scope":"entity.name.selector","settings":{"foreground":"#800000"}},{"scope":"entity.other.attribute-name","settings":{"foreground":"#ff0000"}},{"scope":["entity.other.attribute-name.class.css","entity.other.attribute-name.class.mixin.css","entity.other.attribute-name.id.css","entity.other.attribute-name.parent-selector.css","entity.other.attribute-name.pseudo-class.css","entity.other.attribute-name.pseudo-element.css","source.css.less entity.other.attribute-name.id","entity.other.attribute-name.attribute.scss","entity.other.attribute-name.scss"],"settings":{"foreground":"#800000"}},{"scope":"invalid","settings":{"foreground":"#cd3131"}},{"scope":"markup.underline","settings":{"fontStyle":"underline"}},{"scope":"markup.bold","settings":{"fontStyle":"bold","foreground":"#000080"}},{"scope":"markup.heading","settings":{"fontStyle":"bold","foreground":"#800000"}},{"scope":"markup.italic","settings":{"fontStyle":"italic"}},{"scope":"markup.inserted","settings":{"foreground":"#098658"}},{"scope":"markup.deleted","settings":{"foreground":"#a31515"}},{"scope":"markup.changed","settings":{"foreground":"#0451a5"}},{"scope":["punctuation.definition.quote.begin.markdown","punctuation.definition.list.begin.markdown"],"settings":{"foreground":"#0451a5"}},{"scope":"markup.inline.raw","settings":{"foreground":"#800000"}},{"name":"brackets of XML/HTML tags","scope":"punctuation.definition.tag","settings":{"foreground":"#800000"}},{"scope":"meta.preprocessor","settings":{"foreground":"#0000ff"}},{"scope":"meta.preprocessor.string","settings":{"foreground":"#a31515"}},{"scope":"meta.preprocessor.numeric","settings":{"foreground":"#098658"}},{"scope":"meta.structure.dictionary.key.python","settings":{"foreground":"#0451a5"}},{"scope":"storage","settings":{"foreground":"#0000ff"}},{"scope":"storage.type","settings":{"foreground":"#0000ff"}},{"scope":"storage.modifier","settings":{"foreground":"#0000ff"}},{"scope":"string","settings":{"foreground":"#a31515"}},{"scope":["string.comment.buffered.block.pug","string.quoted.pug","string.interpolated.pug","string.unquoted.plain.in.yaml","string.unquoted.plain.out.yaml","string.unquoted.block.yaml","string.quoted.single.yaml","string.quoted.double.xml","string.quoted.single.xml","string.unquoted.cdata.xml","string.quoted.double.html","string.quoted.single.html","string.unquoted.html","string.quoted.single.handlebars","string.quoted.double.handlebars"],"settings":{"foreground":"#0000ff"}},{"scope":"string.regexp","settings":{"foreground":"#811f3f"}},{"name":"String interpolation","scope":["punctuation.definition.template-expression.begin","punctuation.definition.template-expression.end","punctuation.section.embedded"],"settings":{"foreground":"#0000ff"}},{"name":"Reset JavaScript string interpolation expression","scope":["meta.template.expression"],"settings":{"foreground":"#000000"}},{"scope":["support.constant.property-value","support.constant.font-name","support.constant.media-type","support.constant.media","constant.other.color.rgb-value","constant.other.rgb-value","support.constant.color"],"settings":{"foreground":"#0451a5"}},{"scope":["support.type.vendored.property-name","support.type.property-name","variable.css","variable.scss","variable.other.less","source.coffee.embedded"],"settings":{"foreground":"#ff0000"}},{"scope":["support.type.property-name.json"],"settings":{"foreground":"#0451a5"}},{"scope":"keyword","settings":{"foreground":"#0000ff"}},{"scope":"keyword.control","settings":{"foreground":"#0000ff"}},{"scope":"keyword.operator","settings":{"foreground":"#000000"}},{"scope":["keyword.operator.new","keyword.operator.expression","keyword.operator.cast","keyword.operator.sizeof","keyword.operator.instanceof","keyword.operator.logical.python"],"settings":{"foreground":"#0000ff"}},{"scope":"keyword.other.unit","settings":{"foreground":"#098658"}},{"scope":["punctuation.section.embedded.begin.php","punctuation.section.embedded.end.php"],"settings":{"foreground":"#800000"}},{"scope":"support.function.git-rebase","settings":{"foreground":"#0451a5"}},{"scope":"constant.sha.git-rebase","settings":{"foreground":"#098658"}},{"name":"coloring of the Java import and package identifiers","scope":["storage.modifier.import.java","variable.language.wildcard.java","storage.modifier.package.java"],"settings":{"foreground":"#000000"}},{"name":"this.self","scope":"variable.language","settings":{"foreground":"#0000ff"}},{"name":"Function declarations","scope":["entity.name.function","support.function","support.constant.handlebars"],"settings":{"foreground":"#795E26"}},{"name":"Types declaration and references","scope":["meta.return-type","support.class","support.type","entity.name.type","entity.name.class","storage.type.numeric.go","storage.type.byte.go","storage.type.boolean.go","storage.type.string.go","storage.type.uintptr.go","storage.type.error.go","storage.type.rune.go","storage.type.cs","storage.type.generic.cs","storage.type.modifier.cs","storage.type.variable.cs","storage.type.annotation.java","storage.type.generic.java","storage.type.java","storage.type.object.array.java","storage.type.primitive.array.java","storage.type.primitive.java","storage.type.token.java","storage.type.groovy","storage.type.annotation.groovy","storage.type.parameters.groovy","storage.type.generic.groovy","storage.type.object.array.groovy","storage.type.primitive.array.groovy","storage.type.primitive.groovy"],"settings":{"foreground":"#267f99"}},{"name":"Types declaration and references, TS grammar specific","scope":["meta.type.cast.expr","meta.type.new.expr","support.constant.math","support.constant.dom","support.constant.json","entity.other.inherited-class"],"settings":{"foreground":"#267f99"}},{"name":"Control flow keywords","scope":"keyword.control","settings":{"foreground":"#AF00DB"}},{"name":"Variable and parameter name","scope":["variable","meta.definition.variable.name","support.variable","entity.name.variable"],"settings":{"foreground":"#001080"}},{"name":"Object keys, TS grammar specific","scope":["meta.object-literal.key"],"settings":{"foreground":"#001080"}},{"name":"CSS property value","scope":["support.constant.property-value","support.constant.font-name","support.constant.media-type","support.constant.media","constant.other.color.rgb-value","constant.other.rgb-value","support.constant.color"],"settings":{"foreground":"#0451a5"}},{"name":"Regular expression groups","scope":["punctuation.definition.group.regexp","punctuation.definition.group.assertion.regexp","punctuation.definition.character-class.regexp","punctuation.character.set.begin.regexp","punctuation.character.set.end.regexp","keyword.operator.negation.regexp","support.other.parenthesis.regexp"],"settings":{"foreground":"#d16969"}},{"scope":["constant.character.character-class.regexp","constant.other.character-class.set.regexp","constant.other.character-class.regexp","constant.character.set.regexp"],"settings":{"foreground":"#811f3f"}},{"scope":"keyword.operator.quantifier.regexp","settings":{"foreground":"#000000"}},{"scope":["keyword.operator.or.regexp","keyword.control.anchor.regexp"],"settings":{"foreground":"#ff0000"}},{"scope":"constant.character","settings":{"foreground":"#0000ff"}},{"scope":"constant.character.escape","settings":{"foreground":"#ff0000"}},{"scope":"token.info-token","settings":{"foreground":"#316bcd"}},{"scope":"token.warn-token","settings":{"foreground":"#cd9731"}},{"scope":"token.error-token","settings":{"foreground":"#cd3131"}},{"scope":"token.debug-token","settings":{"foreground":"#800080"}}],"extensionData":{"extensionId":"vscode.theme-defaults","extensionPublisher":"vscode","extensionName":"theme-defaults","extensionIsBuiltin":true},"colorMap":{"editor.background":"#ffffff","editor.foreground":"#000000","editor.inactiveSelectionBackground":"#e5ebf1","editorIndentGuide.background":"#d3d3d3","editorIndentGuide.activeBackground":"#939393","editor.selectionHighlightBackground":"#add6ff4d","editorSuggestWidget.background":"#f3f3f3","activityBarBadge.background":"#007acc","sideBarTitle.foreground":"#6f6f6f","list.hoverBackground":"#e8e8e8","input.placeholderForeground":"#767676","settings.textInputBorder":"#cecece","settings.numberInputBorder":"#cecece"}}'); @@ -587,7 +526,7 @@ suite('SQLite Storage Library', function () { items3.set('very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.', 'is long'); let storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await Promise.all([ await storage.updateItems({ insert: items1 }), @@ -595,28 +534,28 @@ suite('SQLite Storage Library', function () { await storage.updateItems({ insert: items3 }) ]); - equal(await storage.checkIntegrity(true), 'ok'); - equal(await storage.checkIntegrity(false), 'ok'); + strictEqual(await storage.checkIntegrity(true), 'ok'); + strictEqual(await storage.checkIntegrity(false), 'ok'); storedItems = await storage.getItems(); - equal(storedItems.size, items1.size + items2.size + items3.size); + strictEqual(storedItems.size, items1.size + items2.size + items3.size); const items1Keys: string[] = []; items1.forEach((value, key) => { items1Keys.push(key); - equal(storedItems.get(key), value); + strictEqual(storedItems.get(key), value); }); const items2Keys: string[] = []; items2.forEach((value, key) => { items2Keys.push(key); - equal(storedItems.get(key), value); + strictEqual(storedItems.get(key), value); }); const items3Keys: string[] = []; items3.forEach((value, key) => { items3Keys.push(key); - equal(storedItems.get(key), value); + strictEqual(storedItems.get(key), value); }); await Promise.all([ @@ -626,7 +565,7 @@ suite('SQLite Storage Library', function () { ]); storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await Promise.all([ await storage.updateItems({ insert: items1 }), @@ -638,28 +577,20 @@ suite('SQLite Storage Library', function () { ]); storedItems = await storage.getItems(); - equal(storedItems.size, items1.size + items2.size + items3.size); + strictEqual(storedItems.size, items1.size + items2.size + items3.size); await storage.close(); - storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db')); + storage = new SQLiteStorageDatabase(join(testdir, 'storage.db')); storedItems = await storage.getItems(); - equal(storedItems.size, items1.size + items2.size + items3.size); + strictEqual(storedItems.size, items1.size + items2.size + items3.size); await storage.close(); - - await rimraf(storageDir, RimRafMode.MOVE); }); test('very large item value', async function () { - this.timeout(20000); - - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - let storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db')); + let storage = new SQLiteStorageDatabase(join(testdir, 'storage.db')); const items = new Map(); items.set('colorthemedata', '{"id":"vs vscode-theme-defaults-themes-light_plus-json","label":"Light+ (default light)","settingsId":"Default Light+","selector":"vs.vscode-theme-defaults-themes-light_plus-json","themeTokenColors":[{"settings":{"foreground":"#000000ff","background":"#ffffffff"}},{"scope":["meta.embedded","source.groovy.embedded"],"settings":{"foreground":"#000000ff"}},{"scope":"emphasis","settings":{"fontStyle":"italic"}},{"scope":"strong","settings":{"fontStyle":"bold"}},{"scope":"meta.diff.header","settings":{"foreground":"#000080"}},{"scope":"comment","settings":{"foreground":"#008000"}},{"scope":"constant.language","settings":{"foreground":"#0000ff"}},{"scope":["constant.numeric"],"settings":{"foreground":"#098658"}},{"scope":"constant.regexp","settings":{"foreground":"#811f3f"}},{"name":"css tags in selectors, xml tags","scope":"entity.name.tag","settings":{"foreground":"#800000"}},{"scope":"entity.name.selector","settings":{"foreground":"#800000"}},{"scope":"entity.other.attribute-name","settings":{"foreground":"#ff0000"}},{"scope":["entity.other.attribute-name.class.css","entity.other.attribute-name.class.mixin.css","entity.other.attribute-name.id.css","entity.other.attribute-name.parent-selector.css","entity.other.attribute-name.pseudo-class.css","entity.other.attribute-name.pseudo-element.css","source.css.less entity.other.attribute-name.id","entity.other.attribute-name.attribute.scss","entity.other.attribute-name.scss"],"settings":{"foreground":"#800000"}},{"scope":"invalid","settings":{"foreground":"#cd3131"}},{"scope":"markup.underline","settings":{"fontStyle":"underline"}},{"scope":"markup.bold","settings":{"fontStyle":"bold","foreground":"#000080"}},{"scope":"markup.heading","settings":{"fontStyle":"bold","foreground":"#800000"}},{"scope":"markup.italic","settings":{"fontStyle":"italic"}},{"scope":"markup.inserted","settings":{"foreground":"#098658"}},{"scope":"markup.deleted","settings":{"foreground":"#a31515"}},{"scope":"markup.changed","settings":{"foreground":"#0451a5"}},{"scope":["punctuation.definition.quote.begin.markdown","punctuation.definition.list.begin.markdown"],"settings":{"foreground":"#0451a5"}},{"scope":"markup.inline.raw","settings":{"foreground":"#800000"}},{"name":"brackets of XML/HTML tags","scope":"punctuation.definition.tag","settings":{"foreground":"#800000"}},{"scope":"meta.preprocessor","settings":{"foreground":"#0000ff"}},{"scope":"meta.preprocessor.string","settings":{"foreground":"#a31515"}},{"scope":"meta.preprocessor.numeric","settings":{"foreground":"#098658"}},{"scope":"meta.structure.dictionary.key.python","settings":{"foreground":"#0451a5"}},{"scope":"storage","settings":{"foreground":"#0000ff"}},{"scope":"storage.type","settings":{"foreground":"#0000ff"}},{"scope":"storage.modifier","settings":{"foreground":"#0000ff"}},{"scope":"string","settings":{"foreground":"#a31515"}},{"scope":["string.comment.buffered.block.pug","string.quoted.pug","string.interpolated.pug","string.unquoted.plain.in.yaml","string.unquoted.plain.out.yaml","string.unquoted.block.yaml","string.quoted.single.yaml","string.quoted.double.xml","string.quoted.single.xml","string.unquoted.cdata.xml","string.quoted.double.html","string.quoted.single.html","string.unquoted.html","string.quoted.single.handlebars","string.quoted.double.handlebars"],"settings":{"foreground":"#0000ff"}},{"scope":"string.regexp","settings":{"foreground":"#811f3f"}},{"name":"String interpolation","scope":["punctuation.definition.template-expression.begin","punctuation.definition.template-expression.end","punctuation.section.embedded"],"settings":{"foreground":"#0000ff"}},{"name":"Reset JavaScript string interpolation expression","scope":["meta.template.expression"],"settings":{"foreground":"#000000"}},{"scope":["support.constant.property-value","support.constant.font-name","support.constant.media-type","support.constant.media","constant.other.color.rgb-value","constant.other.rgb-value","support.constant.color"],"settings":{"foreground":"#0451a5"}},{"scope":["support.type.vendored.property-name","support.type.property-name","variable.css","variable.scss","variable.other.less","source.coffee.embedded"],"settings":{"foreground":"#ff0000"}},{"scope":["support.type.property-name.json"],"settings":{"foreground":"#0451a5"}},{"scope":"keyword","settings":{"foreground":"#0000ff"}},{"scope":"keyword.control","settings":{"foreground":"#0000ff"}},{"scope":"keyword.operator","settings":{"foreground":"#000000"}},{"scope":["keyword.operator.new","keyword.operator.expression","keyword.operator.cast","keyword.operator.sizeof","keyword.operator.instanceof","keyword.operator.logical.python"],"settings":{"foreground":"#0000ff"}},{"scope":"keyword.other.unit","settings":{"foreground":"#098658"}},{"scope":["punctuation.section.embedded.begin.php","punctuation.section.embedded.end.php"],"settings":{"foreground":"#800000"}},{"scope":"support.function.git-rebase","settings":{"foreground":"#0451a5"}},{"scope":"constant.sha.git-rebase","settings":{"foreground":"#098658"}},{"name":"coloring of the Java import and package identifiers","scope":["storage.modifier.import.java","variable.language.wildcard.java","storage.modifier.package.java"],"settings":{"foreground":"#000000"}},{"name":"this.self","scope":"variable.language","settings":{"foreground":"#0000ff"}},{"name":"Function declarations","scope":["entity.name.function","support.function","support.constant.handlebars"],"settings":{"foreground":"#795E26"}},{"name":"Types declaration and references","scope":["meta.return-type","support.class","support.type","entity.name.type","entity.name.class","storage.type.numeric.go","storage.type.byte.go","storage.type.boolean.go","storage.type.string.go","storage.type.uintptr.go","storage.type.error.go","storage.type.rune.go","storage.type.cs","storage.type.generic.cs","storage.type.modifier.cs","storage.type.variable.cs","storage.type.annotation.java","storage.type.generic.java","storage.type.java","storage.type.object.array.java","storage.type.primitive.array.java","storage.type.primitive.java","storage.type.token.java","storage.type.groovy","storage.type.annotation.groovy","storage.type.parameters.groovy","storage.type.generic.groovy","storage.type.object.array.groovy","storage.type.primitive.array.groovy","storage.type.primitive.groovy"],"settings":{"foreground":"#267f99"}},{"name":"Types declaration and references, TS grammar specific","scope":["meta.type.cast.expr","meta.type.new.expr","support.constant.math","support.constant.dom","support.constant.json","entity.other.inherited-class"],"settings":{"foreground":"#267f99"}},{"name":"Control flow keywords","scope":"keyword.control","settings":{"foreground":"#AF00DB"}},{"name":"Variable and parameter name","scope":["variable","meta.definition.variable.name","support.variable","entity.name.variable"],"settings":{"foreground":"#001080"}},{"name":"Object keys, TS grammar specific","scope":["meta.object-literal.key"],"settings":{"foreground":"#001080"}},{"name":"CSS property value","scope":["support.constant.property-value","support.constant.font-name","support.constant.media-type","support.constant.media","constant.other.color.rgb-value","constant.other.rgb-value","support.constant.color"],"settings":{"foreground":"#0451a5"}},{"name":"Regular expression groups","scope":["punctuation.definition.group.regexp","punctuation.definition.group.assertion.regexp","punctuation.definition.character-class.regexp","punctuation.character.set.begin.regexp","punctuation.character.set.end.regexp","keyword.operator.negation.regexp","support.other.parenthesis.regexp"],"settings":{"foreground":"#d16969"}},{"scope":["constant.character.character-class.regexp","constant.other.character-class.set.regexp","constant.other.character-class.regexp","constant.character.set.regexp"],"settings":{"foreground":"#811f3f"}},{"scope":"keyword.operator.quantifier.regexp","settings":{"foreground":"#000000"}},{"scope":["keyword.operator.or.regexp","keyword.control.anchor.regexp"],"settings":{"foreground":"#ff0000"}},{"scope":"constant.character","settings":{"foreground":"#0000ff"}},{"scope":"constant.character.escape","settings":{"foreground":"#ff0000"}},{"scope":"token.info-token","settings":{"foreground":"#316bcd"}},{"scope":"token.warn-token","settings":{"foreground":"#cd9731"}},{"scope":"token.error-token","settings":{"foreground":"#cd3131"}},{"scope":"token.debug-token","settings":{"foreground":"#800080"}}],"extensionData":{"extensionId":"vscode.theme-defaults","extensionPublisher":"vscode","extensionName":"theme-defaults","extensionIsBuiltin":true},"colorMap":{"editor.background":"#ffffff","editor.foreground":"#000000","editor.inactiveSelectionBackground":"#e5ebf1","editorIndentGuide.background":"#d3d3d3","editorIndentGuide.activeBackground":"#939393","editor.selectionHighlightBackground":"#add6ff4d","editorSuggestWidget.background":"#f3f3f3","activityBarBadge.background":"#007acc","sideBarTitle.foreground":"#6f6f6f","list.hoverBackground":"#e8e8e8","input.placeholderForeground":"#767676","settings.textInputBorder":"#cecece","settings.numberInputBorder":"#cecece"}}'); @@ -675,9 +606,9 @@ suite('SQLite Storage Library', function () { await storage.updateItems({ insert: items }); let storedItems = await storage.getItems(); - equal(items.get('colorthemedata'), storedItems.get('colorthemedata')); - equal(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache')); - equal(items.get('super.large.string'), storedItems.get('super.large.string')); + strictEqual(items.get('colorthemedata'), storedItems.get('colorthemedata')); + strictEqual(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache')); + strictEqual(items.get('super.large.string'), storedItems.get('super.large.string')); uuid = generateUuid(); value = []; @@ -689,27 +620,23 @@ suite('SQLite Storage Library', function () { await storage.updateItems({ insert: items }); storedItems = await storage.getItems(); - equal(items.get('colorthemedata'), storedItems.get('colorthemedata')); - equal(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache')); - equal(items.get('super.large.string'), storedItems.get('super.large.string')); + strictEqual(items.get('colorthemedata'), storedItems.get('colorthemedata')); + strictEqual(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache')); + strictEqual(items.get('super.large.string'), storedItems.get('super.large.string')); const toDelete = new Set(); toDelete.add('super.large.string'); await storage.updateItems({ delete: toDelete }); storedItems = await storage.getItems(); - equal(items.get('colorthemedata'), storedItems.get('colorthemedata')); - equal(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache')); + strictEqual(items.get('colorthemedata'), storedItems.get('colorthemedata')); + strictEqual(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache')); ok(!storedItems.get('super.large.string')); await storage.close(); - - await rimraf(storageDir, RimRafMode.MOVE); }); test('multiple concurrent writes execute in sequence', async () => { - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); class TestStorage extends Storage { getStorage(): IStorageDatabase { @@ -717,7 +644,7 @@ suite('SQLite Storage Library', function () { } } - const storage = new TestStorage(new SQLiteStorageDatabase(join(storageDir, 'storage.db'))); + const storage = new TestStorage(new SQLiteStorageDatabase(join(testdir, 'storage.db'))); await storage.init(); @@ -750,26 +677,20 @@ suite('SQLite Storage Library', function () { await storage.set('some/foo3/path', 'some/bar/path'); const items = await storage.getStorage().getItems(); - equal(items.get('foo'), 'bar'); - equal(items.get('some/foo/path'), 'some/bar/path'); - equal(items.has('foo1'), false); - equal(items.has('some/foo1/path'), false); - equal(items.get('foo2'), 'bar'); - equal(items.get('some/foo2/path'), 'some/bar/path'); - equal(items.get('foo3'), 'bar'); - equal(items.get('some/foo3/path'), 'some/bar/path'); + strictEqual(items.get('foo'), 'bar'); + strictEqual(items.get('some/foo/path'), 'some/bar/path'); + strictEqual(items.has('foo1'), false); + strictEqual(items.has('some/foo1/path'), false); + strictEqual(items.get('foo2'), 'bar'); + strictEqual(items.get('some/foo2/path'), 'some/bar/path'); + strictEqual(items.get('foo3'), 'bar'); + strictEqual(items.get('some/foo3/path'), 'some/bar/path'); await storage.close(); - - await rimraf(storageDir, RimRafMode.MOVE); }); test('lots of INSERT & DELETE (below inline max)', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - const storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db')); + const storage = new SQLiteStorageDatabase(join(testdir, 'storage.db')); const items = new Map(); const keys: Set = new Set(); @@ -784,24 +705,18 @@ suite('SQLite Storage Library', function () { await storage.updateItems({ insert: items }); let storedItems = await storage.getItems(); - equal(storedItems.size, items.size); + strictEqual(storedItems.size, items.size); await storage.updateItems({ delete: keys }); storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await storage.close(); - - await rimraf(storageDir, RimRafMode.MOVE); }); test('lots of INSERT & DELETE (above inline max)', async () => { - const storageDir = uniqueStorageDir(); - - await mkdirp(storageDir); - - const storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db')); + const storage = new SQLiteStorageDatabase(join(testdir, 'storage.db')); const items = new Map(); const keys: Set = new Set(); @@ -816,15 +731,13 @@ suite('SQLite Storage Library', function () { await storage.updateItems({ insert: items }); let storedItems = await storage.getItems(); - equal(storedItems.size, items.size); + strictEqual(storedItems.size, items.size); await storage.updateItems({ delete: keys }); storedItems = await storage.getItems(); - equal(storedItems.size, 0); + strictEqual(storedItems.size, 0); await storage.close(); - - await rimraf(storageDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index 7805b86b05..f9c22ec75e 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -33,28 +33,28 @@ suite('Actionbar', () => { let a2 = new Action('a2'); actionbar.push(a1); - assert.equal(actionbar.hasAction(a1), true); - assert.equal(actionbar.hasAction(a2), false); + assert.strictEqual(actionbar.hasAction(a1), true); + assert.strictEqual(actionbar.hasAction(a2), false); actionbar.pull(0); - assert.equal(actionbar.hasAction(a1), false); + assert.strictEqual(actionbar.hasAction(a1), false); actionbar.push(a1, { index: 1 }); actionbar.push(a2, { index: 0 }); - assert.equal(actionbar.hasAction(a1), true); - assert.equal(actionbar.hasAction(a2), true); + assert.strictEqual(actionbar.hasAction(a1), true); + assert.strictEqual(actionbar.hasAction(a2), true); actionbar.pull(0); - assert.equal(actionbar.hasAction(a1), true); - assert.equal(actionbar.hasAction(a2), false); + assert.strictEqual(actionbar.hasAction(a1), true); + assert.strictEqual(actionbar.hasAction(a2), false); actionbar.pull(0); - assert.equal(actionbar.hasAction(a1), false); - assert.equal(actionbar.hasAction(a2), false); + assert.strictEqual(actionbar.hasAction(a1), false); + assert.strictEqual(actionbar.hasAction(a2), false); actionbar.push(a1); - assert.equal(actionbar.hasAction(a1), true); + assert.strictEqual(actionbar.hasAction(a1), true); actionbar.clear(); - assert.equal(actionbar.hasAction(a1), false); + assert.strictEqual(actionbar.hasAction(a1), false); }); }); diff --git a/src/vs/base/test/browser/codicons.test.ts b/src/vs/base/test/browser/codicons.test.ts deleted file mode 100644 index beeb5be101..0000000000 --- a/src/vs/base/test/browser/codicons.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { renderCodicons } from 'vs/base/browser/codicons'; -import * as assert from 'assert'; - -suite('renderCodicons', () => { - - test('no codicons', () => { - const result = renderCodicons(' hello World .'); - - assert.equal(elementsToString(result), ' hello World .'); - }); - - test('codicon only', () => { - const result = renderCodicons('$(alert)'); - - assert.equal(elementsToString(result), ''); - }); - - test('codicon and non-codicon strings', () => { - const result = renderCodicons(` $(alert) Unresponsive`); - - assert.equal(elementsToString(result), ' Unresponsive'); - }); - - test('multiple codicons', () => { - const result = renderCodicons('$(check)$(error)'); - - assert.equal(elementsToString(result), ''); - }); - - test('escaped codicon', () => { - const result = renderCodicons('\\$(escaped)'); - - assert.equal(elementsToString(result), '$(escaped)'); - }); - - test('codicon with animation', () => { - const result = renderCodicons('$(zip~anim)'); - - assert.equal(elementsToString(result), ''); - }); - - const elementsToString = (elements: Array): string => { - return elements - .map(elem => elem instanceof HTMLElement ? elem.outerHTML : elem) - .reduce((a, b) => a + b, ''); - }; -}); diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index ffa59bd7ad..f6ef36bfbe 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -123,8 +123,8 @@ suite('Comparers', () => { // name-only comparisions assert(compareFileExtensions('a', 'A') !== compareLocale('a', 'A'), 'the same letter of different case does not sort by locale'); assert(compareFileExtensions('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter of different case does not sort by locale'); - assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensions), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); - assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensions), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents do not sort in locale order'); + assert.notDeepStrictEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensions), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); + assert.notDeepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensions), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents do not sort in locale order'); // name plus extension comparisons assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale'); @@ -197,8 +197,7 @@ suite('Comparers', () => { // name-only comparisons assert(compareFileNamesDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); assert(compareFileNamesDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale'); - // assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); - assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesDefault), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesDefault), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); // numeric comparisons assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); @@ -259,8 +258,7 @@ suite('Comparers', () => { // name-only comparisons assert(compareFileExtensionsDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); assert(compareFileExtensionsDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale'); - // assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); - assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsDefault), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); + assert.deepStrictEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsDefault), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); // name plus extension comparisons assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 11b8ef5bce..e6f0831f85 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -30,7 +30,7 @@ suite('dom', () => { assert(element.classList.contains('far')); assert(!element.classList.contains('boo')); assert(element.classList.contains('foobar')); - assert.equal(element.className, 'foobar far'); + assert.strictEqual(element.className, 'foobar far'); element = document.createElement('div'); element.className = 'foobar boo far'; @@ -39,19 +39,19 @@ suite('dom', () => { assert(!element.classList.contains('far')); assert(element.classList.contains('boo')); assert(element.classList.contains('foobar')); - assert.equal(element.className, 'foobar boo'); + assert.strictEqual(element.className, 'foobar boo'); element.classList.remove('boo'); assert(!element.classList.contains('far')); assert(!element.classList.contains('boo')); assert(element.classList.contains('foobar')); - assert.equal(element.className, 'foobar'); + assert.strictEqual(element.className, 'foobar'); element.classList.remove('foobar'); assert(!element.classList.contains('far')); assert(!element.classList.contains('boo')); assert(!element.classList.contains('foobar')); - assert.equal(element.className, ''); + assert.strictEqual(element.className, ''); }); test.skip('removeClass should consider hyphens', function () { // {{SQL CARBON EDIT}} skip test @@ -73,8 +73,9 @@ suite('dom', () => { }); test('multibyteAwareBtoa', () => { - assert.equal(dom.multibyteAwareBtoa('hello world'), dom.multibyteAwareBtoa('hello world')); - assert.ok(dom.multibyteAwareBtoa('平仮名')); + assert.ok(dom.multibyteAwareBtoa('hello world').length > 0); + assert.ok(dom.multibyteAwareBtoa('平仮名').length > 0); + assert.ok(dom.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 }); suite('$', () => { @@ -82,7 +83,7 @@ suite('dom', () => { const div = $('div'); assert(div); assert(div instanceof HTMLElement); - assert.equal(div.tagName, 'DIV'); + assert.strictEqual(div.tagName, 'DIV'); assert(!div.firstChild); }); @@ -90,42 +91,42 @@ suite('dom', () => { const div = $('div#foo'); assert(div); assert(div instanceof HTMLElement); - assert.equal(div.tagName, 'DIV'); - assert.equal(div.id, 'foo'); + assert.strictEqual(div.tagName, 'DIV'); + assert.strictEqual(div.id, 'foo'); }); test('should buld nodes with class-name', () => { const div = $('div.foo'); assert(div); assert(div instanceof HTMLElement); - assert.equal(div.tagName, 'DIV'); - assert.equal(div.className, 'foo'); + assert.strictEqual(div.tagName, 'DIV'); + assert.strictEqual(div.className, 'foo'); }); test('should build nodes with attributes', () => { let div = $('div', { class: 'test' }); - assert.equal(div.className, 'test'); + assert.strictEqual(div.className, 'test'); div = $('div', undefined); - assert.equal(div.className, ''); + assert.strictEqual(div.className, ''); }); test('should build nodes with children', () => { let div = $('div', undefined, $('span', { id: 'demospan' })); let firstChild = div.firstChild as HTMLElement; - assert.equal(firstChild.tagName, 'SPAN'); - assert.equal(firstChild.id, 'demospan'); + assert.strictEqual(firstChild.tagName, 'SPAN'); + assert.strictEqual(firstChild.id, 'demospan'); div = $('div', undefined, 'hello'); - assert.equal(div.firstChild && div.firstChild.textContent, 'hello'); + assert.strictEqual(div.firstChild && div.firstChild.textContent, 'hello'); }); test('should build nodes with text children', () => { let div = $('div', undefined, 'foobar'); let firstChild = div.firstChild as HTMLElement; - assert.equal(firstChild.tagName, undefined); - assert.equal(firstChild.textContent, 'foobar'); + assert.strictEqual(firstChild.tagName, undefined); + assert.strictEqual(firstChild.textContent, 'foobar'); }); }); }); diff --git a/src/vs/base/test/browser/hash.test.ts b/src/vs/base/test/browser/hash.test.ts index 4cd6dd5d6b..615f6dbd68 100644 --- a/src/vs/base/test/browser/hash.test.ts +++ b/src/vs/base/test/browser/hash.test.ts @@ -9,53 +9,53 @@ import { sha1Hex } from 'vs/base/browser/hash'; suite('Hash', () => { test('string', () => { - assert.equal(hash('hello'), hash('hello')); - assert.notEqual(hash('hello'), hash('world')); - assert.notEqual(hash('hello'), hash('olleh')); - assert.notEqual(hash('hello'), hash('Hello')); - assert.notEqual(hash('hello'), hash('Hello ')); - assert.notEqual(hash('h'), hash('H')); - assert.notEqual(hash('-'), hash('_')); + assert.strictEqual(hash('hello'), hash('hello')); + assert.notStrictEqual(hash('hello'), hash('world')); + assert.notStrictEqual(hash('hello'), hash('olleh')); + assert.notStrictEqual(hash('hello'), hash('Hello')); + assert.notStrictEqual(hash('hello'), hash('Hello ')); + assert.notStrictEqual(hash('h'), hash('H')); + assert.notStrictEqual(hash('-'), hash('_')); }); test('number', () => { - assert.equal(hash(1), hash(1)); - assert.notEqual(hash(0), hash(1)); - assert.notEqual(hash(1), hash(-1)); - assert.notEqual(hash(0x12345678), hash(0x123456789)); + assert.strictEqual(hash(1), hash(1)); + assert.notStrictEqual(hash(0), hash(1)); + assert.notStrictEqual(hash(1), hash(-1)); + assert.notStrictEqual(hash(0x12345678), hash(0x123456789)); }); test('boolean', () => { - assert.equal(hash(true), hash(true)); - assert.notEqual(hash(true), hash(false)); + assert.strictEqual(hash(true), hash(true)); + assert.notStrictEqual(hash(true), hash(false)); }); test('array', () => { - assert.equal(hash([1, 2, 3]), hash([1, 2, 3])); - assert.equal(hash(['foo', 'bar']), hash(['foo', 'bar'])); - assert.equal(hash([]), hash([])); - assert.equal(hash([]), hash(new Array())); - assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo'])); - assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', null])); - assert.notEqual(hash(['foo', 'bar', null]), hash(['bar', 'foo', null])); - assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', undefined])); - assert.notEqual(hash(['foo', 'bar', undefined]), hash(['bar', 'foo', undefined])); - assert.notEqual(hash(['foo', 'bar', null]), hash(['foo', 'bar', undefined])); + assert.strictEqual(hash([1, 2, 3]), hash([1, 2, 3])); + assert.strictEqual(hash(['foo', 'bar']), hash(['foo', 'bar'])); + assert.strictEqual(hash([]), hash([])); + assert.strictEqual(hash([]), hash(new Array())); + assert.notStrictEqual(hash(['foo', 'bar']), hash(['bar', 'foo'])); + assert.notStrictEqual(hash(['foo', 'bar']), hash(['bar', 'foo', null])); + assert.notStrictEqual(hash(['foo', 'bar', null]), hash(['bar', 'foo', null])); + assert.notStrictEqual(hash(['foo', 'bar']), hash(['bar', 'foo', undefined])); + assert.notStrictEqual(hash(['foo', 'bar', undefined]), hash(['bar', 'foo', undefined])); + assert.notStrictEqual(hash(['foo', 'bar', null]), hash(['foo', 'bar', undefined])); }); test('object', () => { - assert.equal(hash({}), hash({})); - assert.equal(hash({}), hash(Object.create(null))); - assert.equal(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar' })); - assert.equal(hash({ 'foo': 'bar', 'foo2': undefined }), hash({ 'foo2': undefined, 'foo': 'bar' })); - assert.notEqual(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar2' })); - assert.notEqual(hash({}), hash([])); + assert.strictEqual(hash({}), hash({})); + assert.strictEqual(hash({}), hash(Object.create(null))); + assert.strictEqual(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar' })); + assert.strictEqual(hash({ 'foo': 'bar', 'foo2': undefined }), hash({ 'foo2': undefined, 'foo': 'bar' })); + assert.notStrictEqual(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar2' })); + assert.notStrictEqual(hash({}), hash([])); }); test('array - unexpected collision', function () { const a = hash([undefined, undefined, undefined, undefined, undefined]); const b = hash([undefined, undefined, 'HHHHHH', [{ line: 0, character: 0 }, { line: 0, character: 0 }], undefined]); - assert.notEqual(a, b); + assert.notStrictEqual(a, b); }); test('all different', () => { @@ -65,9 +65,9 @@ suite('Hash', () => { ]; const hashes: number[] = candidates.map(hash); for (let i = 0; i < hashes.length; i++) { - assert.equal(hashes[i], hash(candidates[i])); // verify that repeated invocation returns the same hash + assert.strictEqual(hashes[i], hash(candidates[i])); // verify that repeated invocation returns the same hash for (let k = i + 1; k < hashes.length; k++) { - assert.notEqual(hashes[i], hashes[k], `Same hash ${hashes[i]} for ${JSON.stringify(candidates[i])} and ${JSON.stringify(candidates[k])}`); + assert.notStrictEqual(hashes[i], hashes[k], `Same hash ${hashes[i]} for ${JSON.stringify(candidates[i])} and ${JSON.stringify(candidates[k])}`); } } }); @@ -79,11 +79,11 @@ suite('Hash', () => { const hash = new StringSHA1(); hash.update(str); let actual = hash.digest(); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); // Test with crypto.subtle actual = await sha1Hex(str); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } test('sha1-1', () => { diff --git a/src/vs/base/test/browser/highlightedLabel.test.ts b/src/vs/base/test/browser/highlightedLabel.test.ts index 57bf808cd0..84a52f8351 100644 --- a/src/vs/base/test/browser/highlightedLabel.test.ts +++ b/src/vs/base/test/browser/highlightedLabel.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; @@ -13,50 +14,50 @@ suite('HighlightedLabel', () => { }); test('empty label', function () { - assert.equal(label.element.innerHTML, ''); + assert.strictEqual(label.element.innerHTML, ''); }); test('no decorations', function () { label.set('hello'); - assert.equal(label.element.innerHTML, 'hello'); + assert.strictEqual(label.element.innerHTML, 'hello'); }); test('escape html', function () { label.set('helhel<lo'); + assert.strictEqual(label.element.innerHTML, 'hel<lo'); }); test('everything highlighted', function () { label.set('hello', [{ start: 0, end: 5 }]); - assert.equal(label.element.innerHTML, 'hello'); + assert.strictEqual(label.element.innerHTML, 'hello'); }); test('beginning highlighted', function () { label.set('hellothere', [{ start: 0, end: 5 }]); - assert.equal(label.element.innerHTML, 'hellothere'); + assert.strictEqual(label.element.innerHTML, 'hellothere'); }); test('ending highlighted', function () { label.set('goodbye', [{ start: 4, end: 7 }]); - assert.equal(label.element.innerHTML, 'goodbye'); + assert.strictEqual(label.element.innerHTML, 'goodbye'); }); test('middle highlighted', function () { label.set('foobarfoo', [{ start: 3, end: 6 }]); - assert.equal(label.element.innerHTML, 'foobarfoo'); + assert.strictEqual(label.element.innerHTML, 'foobarfoo'); }); test('escapeNewLines', () => { let highlights = [{ start: 0, end: 5 }, { start: 7, end: 9 }, { start: 11, end: 12 }];// before,after,after let escaped = HighlightedLabel.escapeNewLines('ACTION\r\n_TYPE2', highlights); - assert.equal(escaped, 'ACTION\u23CE_TYPE2'); - assert.deepEqual(highlights, [{ start: 0, end: 5 }, { start: 6, end: 8 }, { start: 10, end: 11 }]); + assert.strictEqual(escaped, 'ACTION\u23CE_TYPE2'); + assert.deepStrictEqual(highlights, [{ start: 0, end: 5 }, { start: 6, end: 8 }, { start: 10, end: 11 }]); highlights = [{ start: 5, end: 9 }, { start: 11, end: 12 }];//overlap,after escaped = HighlightedLabel.escapeNewLines('ACTION\r\n_TYPE2', highlights); - assert.equal(escaped, 'ACTION\u23CE_TYPE2'); - assert.deepEqual(highlights, [{ start: 5, end: 8 }, { start: 10, end: 11 }]); + assert.strictEqual(escaped, 'ACTION\u23CE_TYPE2'); + assert.deepStrictEqual(highlights, [{ start: 5, end: 8 }, { start: 10, end: 11 }]); }); }); diff --git a/src/vs/base/test/browser/iconLabels.test.ts b/src/vs/base/test/browser/iconLabels.test.ts new file mode 100644 index 0000000000..020bfec167 --- /dev/null +++ b/src/vs/base/test/browser/iconLabels.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; + +suite('renderLabelWithIcons', () => { + + test('no icons', () => { + const result = renderLabelWithIcons(' hello World .'); + + assert.strictEqual(elementsToString(result), ' hello World .'); + }); + + test('icons only', () => { + const result = renderLabelWithIcons('$(alert)'); + + assert.strictEqual(elementsToString(result), ''); + }); + + test('icon and non-icon strings', () => { + const result = renderLabelWithIcons(` $(alert) Unresponsive`); + + assert.strictEqual(elementsToString(result), ' Unresponsive'); + }); + + test('multiple icons', () => { + const result = renderLabelWithIcons('$(check)$(error)'); + + assert.strictEqual(elementsToString(result), ''); + }); + + test('escaped icons', () => { + const result = renderLabelWithIcons('\\$(escaped)'); + + assert.strictEqual(elementsToString(result), '$(escaped)'); + }); + + test('icon with animation', () => { + const result = renderLabelWithIcons('$(zip~anim)'); + + assert.strictEqual(elementsToString(result), ''); + }); + + const elementsToString = (elements: Array): string => { + return elements + .map(elem => elem instanceof HTMLElement ? elem.outerHTML : elem) + .reduce((a, b) => a + b, ''); + }; +}); diff --git a/src/vs/base/test/browser/ui/contextview/contextview.test.ts b/src/vs/base/test/browser/ui/contextview/contextview.test.ts index 7aae619bfb..82807e32c9 100644 --- a/src/vs/base/test/browser/ui/contextview/contextview.test.ts +++ b/src/vs/base/test/browser/ui/contextview/contextview.test.ts @@ -9,20 +9,20 @@ import { layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/con suite('Contextview', function () { test('layout', () => { - assert.equal(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0); - assert.equal(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50); - assert.equal(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180); + assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0); + assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50); + assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180); - assert.equal(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0); - assert.equal(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30); - assert.equal(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180); + assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0); + assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30); + assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180); - assert.equal(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50); - assert.equal(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100); - assert.equal(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130); + assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50); + assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100); + assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130); - assert.equal(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50); - assert.equal(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30); - assert.equal(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130); + assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50); + assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30); + assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130); }); }); diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts index faaa1f04bd..50b97248ec 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollableElement.test.ts @@ -53,7 +53,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, false); + assert.strictEqual(actual, false); } }); @@ -142,7 +142,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, false); + assert.strictEqual(actual, false); } }); @@ -202,7 +202,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, true); + assert.strictEqual(actual, true); } }); @@ -241,7 +241,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, true); + assert.strictEqual(actual, true); } }); @@ -285,7 +285,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, false); + assert.strictEqual(actual, false); } }); @@ -374,7 +374,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, true); + assert.strictEqual(actual, true); } }); @@ -464,7 +464,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, true); + assert.strictEqual(actual, true); } }); @@ -518,7 +518,7 @@ suite('MouseWheelClassifier', () => { classifier.accept(timestamp, deltaX, deltaY); const actual = classifier.isPhysicalMouseWheel(); - assert.equal(actual, true); + assert.strictEqual(actual, true); } }); }); diff --git a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts index 77d9a3e1bc..d2f4dd6b1e 100644 --- a/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts +++ b/src/vs/base/test/browser/ui/scrollbar/scrollbarState.test.ts @@ -10,52 +10,52 @@ suite('ScrollbarState', () => { test('inflates slider size', () => { let actual = new ScrollbarState(0, 14, 0, 339, 42423, 32787); - assert.equal(actual.getArrowSize(), 0); - assert.equal(actual.getScrollPosition(), 32787); - assert.equal(actual.getRectangleLargeSize(), 339); - assert.equal(actual.getRectangleSmallSize(), 14); - assert.equal(actual.isNeeded(), true); - assert.equal(actual.getSliderSize(), 20); - assert.equal(actual.getSliderPosition(), 249); + assert.strictEqual(actual.getArrowSize(), 0); + assert.strictEqual(actual.getScrollPosition(), 32787); + assert.strictEqual(actual.getRectangleLargeSize(), 339); + assert.strictEqual(actual.getRectangleSmallSize(), 14); + assert.strictEqual(actual.isNeeded(), true); + assert.strictEqual(actual.getSliderSize(), 20); + assert.strictEqual(actual.getSliderPosition(), 249); - assert.equal(actual.getDesiredScrollPositionFromOffset(259), 32849); + assert.strictEqual(actual.getDesiredScrollPositionFromOffset(259), 32849); // 259 is greater than 230 so page down, 32787 + 339 = 33126 - assert.equal(actual.getDesiredScrollPositionFromOffsetPaged(259), 33126); + assert.strictEqual(actual.getDesiredScrollPositionFromOffsetPaged(259), 33126); actual.setScrollPosition(32849); - assert.equal(actual.getArrowSize(), 0); - assert.equal(actual.getScrollPosition(), 32849); - assert.equal(actual.getRectangleLargeSize(), 339); - assert.equal(actual.getRectangleSmallSize(), 14); - assert.equal(actual.isNeeded(), true); - assert.equal(actual.getSliderSize(), 20); - assert.equal(actual.getSliderPosition(), 249); + assert.strictEqual(actual.getArrowSize(), 0); + assert.strictEqual(actual.getScrollPosition(), 32849); + assert.strictEqual(actual.getRectangleLargeSize(), 339); + assert.strictEqual(actual.getRectangleSmallSize(), 14); + assert.strictEqual(actual.isNeeded(), true); + assert.strictEqual(actual.getSliderSize(), 20); + assert.strictEqual(actual.getSliderPosition(), 249); }); test('inflates slider size with arrows', () => { let actual = new ScrollbarState(12, 14, 0, 339, 42423, 32787); - assert.equal(actual.getArrowSize(), 12); - assert.equal(actual.getScrollPosition(), 32787); - assert.equal(actual.getRectangleLargeSize(), 339); - assert.equal(actual.getRectangleSmallSize(), 14); - assert.equal(actual.isNeeded(), true); - assert.equal(actual.getSliderSize(), 20); - assert.equal(actual.getSliderPosition(), 230); + assert.strictEqual(actual.getArrowSize(), 12); + assert.strictEqual(actual.getScrollPosition(), 32787); + assert.strictEqual(actual.getRectangleLargeSize(), 339); + assert.strictEqual(actual.getRectangleSmallSize(), 14); + assert.strictEqual(actual.isNeeded(), true); + assert.strictEqual(actual.getSliderSize(), 20); + assert.strictEqual(actual.getSliderPosition(), 230); - assert.equal(actual.getDesiredScrollPositionFromOffset(240 + 12), 32811); + assert.strictEqual(actual.getDesiredScrollPositionFromOffset(240 + 12), 32811); // 240 + 12 = 252; greater than 230 so page down, 32787 + 339 = 33126 - assert.equal(actual.getDesiredScrollPositionFromOffsetPaged(240 + 12), 33126); + assert.strictEqual(actual.getDesiredScrollPositionFromOffsetPaged(240 + 12), 33126); actual.setScrollPosition(32811); - assert.equal(actual.getArrowSize(), 12); - assert.equal(actual.getScrollPosition(), 32811); - assert.equal(actual.getRectangleLargeSize(), 339); - assert.equal(actual.getRectangleSmallSize(), 14); - assert.equal(actual.isNeeded(), true); - assert.equal(actual.getSliderSize(), 20); - assert.equal(actual.getSliderPosition(), 230); + assert.strictEqual(actual.getArrowSize(), 12); + assert.strictEqual(actual.getScrollPosition(), 32811); + assert.strictEqual(actual.getRectangleLargeSize(), 339); + assert.strictEqual(actual.getRectangleSmallSize(), 14); + assert.strictEqual(actual.isNeeded(), true); + assert.strictEqual(actual.getSliderSize(), 20); + assert.strictEqual(actual.getSliderPosition(), 230); }); }); diff --git a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts index 312745c6c3..57897afd1f 100644 --- a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts @@ -8,6 +8,7 @@ import { compress, ICompressedTreeElement, ICompressedTreeNode, decompress, Comp import { Iterable } from 'vs/base/common/iterator'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IObjectTreeModelSetChildrenOptions } from 'vs/base/browser/ui/tree/objectTreeModel'; interface IResolvedCompressedTreeElement extends ICompressedTreeElement { readonly element: T; @@ -304,6 +305,16 @@ suite('CompressedObjectTree', function () { suite('CompressedObjectTreeModel', function () { + /** + * Calls that test function twice, once with an empty options and + * once with `diffIdentityProvider`. + */ + function withSmartSplice(fn: (options: IObjectTreeModelSetChildrenOptions) => void) { + fn({}); + fn({ diffIdentityProvider: { getId: n => String(n) } }); + } + + test('ctor', () => { const list: ITreeNode>[] = []; const model = new CompressedObjectTreeModel('test', toList(list)); @@ -312,7 +323,7 @@ suite('CompressedObjectTree', function () { assert.equal(model.size, 0); }); - test('flat', () => { + test('flat', () => withSmartSplice(options => { const list: ITreeNode>[] = []; const model = new CompressedObjectTreeModel('test', toList(list)); @@ -320,7 +331,7 @@ suite('CompressedObjectTree', function () { { element: 0 }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(toArray(list), [[0], [1], [2]]); assert.equal(model.size, 3); @@ -329,17 +340,17 @@ suite('CompressedObjectTree', function () { { element: 3 }, { element: 4 }, { element: 5 }, - ]); + ], options); assert.deepEqual(toArray(list), [[3], [4], [5]]); assert.equal(model.size, 3); - model.setChildren(null); + model.setChildren(null, [], options); assert.deepEqual(toArray(list), []); assert.equal(model.size, 0); - }); + })); - test('nested', () => { + test('nested', () => withSmartSplice(options => { const list: ITreeNode>[] = []; const model = new CompressedObjectTreeModel('test', toList(list)); @@ -353,7 +364,7 @@ suite('CompressedObjectTree', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(toArray(list), [[0], [10], [11], [12], [1], [2]]); assert.equal(model.size, 6); @@ -361,21 +372,21 @@ suite('CompressedObjectTree', function () { model.setChildren(12, [ { element: 120 }, { element: 121 } - ]); + ], options); assert.deepEqual(toArray(list), [[0], [10], [11], [12], [120], [121], [1], [2]]); assert.equal(model.size, 8); - model.setChildren(0); + model.setChildren(0, [], options); assert.deepEqual(toArray(list), [[0], [1], [2]]); assert.equal(model.size, 3); - model.setChildren(null); + model.setChildren(null, [], options); assert.deepEqual(toArray(list), []); assert.equal(model.size, 0); - }); + })); - test('compressed', () => { + test('compressed', () => withSmartSplice(options => { const list: ITreeNode>[] = []; const model = new CompressedObjectTreeModel('test', toList(list)); @@ -391,7 +402,7 @@ suite('CompressedObjectTree', function () { }] }] } - ]); + ], options); assert.deepEqual(toArray(list), [[1, 11, 111], [1111], [1112], [1113]]); assert.equal(model.size, 6); @@ -400,21 +411,21 @@ suite('CompressedObjectTree', function () { { element: 111 }, { element: 112 }, { element: 113 }, - ]); + ], options); assert.deepEqual(toArray(list), [[1, 11], [111], [112], [113]]); assert.equal(model.size, 5); model.setChildren(113, [ { element: 1131 } - ]); + ], options); assert.deepEqual(toArray(list), [[1, 11], [111], [112], [113, 1131]]); assert.equal(model.size, 6); model.setChildren(1131, [ { element: 1132 } - ]); + ], options); assert.deepEqual(toArray(list), [[1, 11], [111], [112], [113, 1131, 1132]]); assert.equal(model.size, 7); @@ -422,10 +433,10 @@ suite('CompressedObjectTree', function () { model.setChildren(1131, [ { element: 1132 }, { element: 1133 }, - ]); + ], options); assert.deepEqual(toArray(list), [[1, 11], [111], [112], [113, 1131], [1132], [1133]]); assert.equal(model.size, 8); - }); + })); }); }); diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 5e7a5e3e4b..5ff3b20457 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ITreeNode, ITreeFilter, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; -import { IndexTreeModel, IIndexTreeNode, IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { ITreeNode, ITreeFilter, TreeVisibility, ITreeElement } from 'vs/base/browser/ui/tree/tree'; +import { IndexTreeModel, IIndexTreeNode, IList, IIndexTreeModelSpliceOptions } from 'vs/base/browser/ui/tree/indexTreeModel'; function toList(arr: T[]): IList { return { @@ -20,7 +20,23 @@ function toArray(list: ITreeNode[]): T[] { return list.map(i => i.element); } -suite('IndexTreeModel', function () { + +function toElements(node: ITreeNode): any { + return node.children?.length ? { e: node.element, children: node.children.map(toElements) } : node.element; +} + +const diffIdentityProvider = { getId: (n: number) => String(n) }; + +/** + * Calls that test function twice, once with an empty options and + * once with `diffIdentityProvider`. + */ +function withSmartSplice(fn: (options: IIndexTreeModelSpliceOptions) => void) { + fn({}); + fn({ diffIdentityProvider }); +} + +suite('IndexTreeModel', () => { test('ctor', () => { const list: ITreeNode[] = []; @@ -29,7 +45,7 @@ suite('IndexTreeModel', function () { assert.equal(list.length, 0); }); - test('insert', () => { + test('insert', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -37,7 +53,7 @@ suite('IndexTreeModel', function () { { element: 0 }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 3); assert.deepEqual(list[0].element, 0); @@ -49,9 +65,9 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[2].element, 2); assert.deepEqual(list[2].collapsed, false); assert.deepEqual(list[2].depth, 1); - }); + })); - test('deep insert', function () { + test('deep insert', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -86,9 +102,9 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[5].element, 2); assert.deepEqual(list[5].collapsed, false); assert.deepEqual(list[5].depth, 1); - }); + })); - test('deep insert collapsed', function () { + test('deep insert collapsed', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -102,7 +118,7 @@ suite('IndexTreeModel', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 3); assert.deepEqual(list[0].element, 0); @@ -114,9 +130,9 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[2].element, 2); assert.deepEqual(list[2].collapsed, false); assert.deepEqual(list[2].depth, 1); - }); + })); - test('delete', () => { + test('delete', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -124,11 +140,11 @@ suite('IndexTreeModel', function () { { element: 0 }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 3); - model.splice([1], 1); + model.splice([1], 1, undefined, options); assert.deepEqual(list.length, 2); assert.deepEqual(list[0].element, 0); assert.deepEqual(list[0].collapsed, false); @@ -137,11 +153,11 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[1].collapsed, false); assert.deepEqual(list[1].depth, 1); - model.splice([0], 2); + model.splice([0], 2, undefined, options); assert.deepEqual(list.length, 0); - }); + })); - test('nested delete', function () { + test('nested delete', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -155,11 +171,11 @@ suite('IndexTreeModel', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 6); - model.splice([1], 2); + model.splice([1], 2, undefined, options); assert.deepEqual(list.length, 4); assert.deepEqual(list[0].element, 0); assert.deepEqual(list[0].collapsed, false); @@ -173,9 +189,9 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[3].element, 12); assert.deepEqual(list[3].collapsed, false); assert.deepEqual(list[3].depth, 2); - }); + })); - test('deep delete', function () { + test('deep delete', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -189,11 +205,11 @@ suite('IndexTreeModel', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 6); - model.splice([0], 1); + model.splice([0], 1, undefined, options); assert.deepEqual(list.length, 2); assert.deepEqual(list[0].element, 1); assert.deepEqual(list[0].collapsed, false); @@ -201,9 +217,43 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[1].element, 2); assert.deepEqual(list[1].collapsed, false); assert.deepEqual(list[1].depth, 1); + })); + + test('smart splice deep', () => { + const list: ITreeNode[] = []; + const model = new IndexTreeModel('test', toList(list), -1); + + model.splice([0], 0, [ + { element: 0 }, + { element: 1 }, + { element: 2 }, + { element: 3 }, + ], { diffIdentityProvider }); + + assert.deepStrictEqual(list.filter(l => l.depth === 1).map(toElements), [ + 0, + 1, + 2, + 3, + ]); + + model.splice([0], 3, [ + { element: -0.5 }, + { element: 0, children: [{ element: 0.1 }] }, + { element: 1 }, + { element: 2, children: [{ element: 2.1 }, { element: 2.2, children: [{ element: 2.21 }] }] }, + ], { diffIdentityProvider, diffDepth: Infinity }); + + assert.deepStrictEqual(list.filter(l => l.depth === 1).map(toElements), [ + -0.5, + { e: 0, children: [0.1] }, + 1, + { e: 2, children: [2.1, { e: 2.2, children: [2.21] }] }, + 3, + ]); }); - test('hidden delete', function () { + test('hidden delete', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -217,18 +267,18 @@ suite('IndexTreeModel', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 3); - model.splice([0, 1], 1); + model.splice([0, 1], 1, undefined, options); assert.deepEqual(list.length, 3); - model.splice([0, 0], 2); + model.splice([0, 0], 2, undefined, options); assert.deepEqual(list.length, 3); - }); + })); - test('collapse', () => { + test('collapse', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -242,7 +292,7 @@ suite('IndexTreeModel', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 6); @@ -257,9 +307,33 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[2].element, 2); assert.deepEqual(list[2].collapsed, false); assert.deepEqual(list[2].depth, 1); - }); + })); - test('expand', () => { + test('updates collapsible', () => withSmartSplice(options => { + const list: ITreeNode[] = []; + const model = new IndexTreeModel('test', toList(list), -1); + + model.splice([0], 0, [ + { + element: 0, children: [ + { element: 1 }, + ] + }, + ], options); + + assert.strictEqual(list[0].collapsible, true); + assert.strictEqual(list[1].collapsible, false); + + model.splice([0, 0], 1, [], options); + assert.strictEqual(list[0].collapsible, false); + assert.strictEqual(list[1], undefined); + + model.splice([0, 0], 0, [{ element: 1 }], options); + assert.strictEqual(list[0].collapsible, true); + assert.strictEqual(list[1].collapsible, false); + })); + + test('expand', () => withSmartSplice(options => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -273,7 +347,7 @@ suite('IndexTreeModel', function () { }, { element: 1 }, { element: 2 } - ]); + ], options); assert.deepEqual(list.length, 3); @@ -297,9 +371,52 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[5].element, 2); assert.deepEqual(list[5].collapsed, false); assert.deepEqual(list[5].depth, 1); + })); + + test('smart diff consistency', () => { + const times = 500; + const minEdits = 1; + const maxEdits = 10; + const maxInserts = 5; + + for (let i = 0; i < times; i++) { + const list: ITreeNode[] = []; + const options = { diffIdentityProvider: { getId: (n: number) => String(n) } }; + const model = new IndexTreeModel('test', toList(list), -1); + + const changes = []; + const expected: number[] = []; + let elementCounter = 0; + + for (let edits = Math.random() * (maxEdits - minEdits) + minEdits; edits > 0; edits--) { + const spliceIndex = Math.floor(Math.random() * list.length); + const deleteCount = Math.ceil(Math.random() * (list.length - spliceIndex)); + const insertCount = Math.floor(Math.random() * maxInserts + 1); + + let inserts: ITreeElement[] = []; + for (let i = 0; i < insertCount; i++) { + const element = elementCounter++; + inserts.push({ element, children: [] }); + } + + // move existing items + if (Math.random() < 0.5) { + const elements = list.slice(spliceIndex, spliceIndex + Math.floor(deleteCount / 2)); + inserts.push(...elements.map(({ element }) => ({ element, children: [] }))); + } + + model.splice([spliceIndex], deleteCount, inserts, options); + expected.splice(spliceIndex, deleteCount, ...inserts.map(i => i.element)); + + const listElements = list.map(l => l.element); + changes.push(`splice(${spliceIndex}, ${deleteCount}, [${inserts.map(e => e.element).join(', ')}]) -> ${listElements.join(', ')}`); + + assert.deepStrictEqual(expected, listElements, `Expected ${listElements.join(', ')} to equal ${expected.join(', ')}. Steps:\n\n${changes.join('\n')}`); + } + } }); - test('collapse should recursively adjust visible count', function () { + test('collapse should recursively adjust visible count', () => { const list: ITreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -395,7 +512,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[1].collapsed, false); }); - test('simple filter', function () { + test('simple filter', () => { const list: ITreeNode[] = []; const filter = new class implements ITreeFilter { filter(element: number): TreeVisibility { @@ -429,7 +546,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(toArray(list), [0, 2, 4, 6]); }); - test('recursive filter on initial model', function () { + test('recursive filter on initial model', () => { const list: ITreeNode[] = []; const filter = new class implements ITreeFilter { filter(element: number): TreeVisibility { @@ -451,7 +568,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(toArray(list), []); }); - test('refilter', function () { + test('refilter', () => { const list: ITreeNode[] = []; let shouldFilter = false; const filter = new class implements ITreeFilter { @@ -490,7 +607,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(toArray(list), [0, 1, 2, 3, 4, 5, 6, 7]); }); - test('recursive filter', function () { + test('recursive filter', () => { const list: ITreeNode[] = []; let query = new RegExp(''); const filter = new class implements ITreeFilter { @@ -536,7 +653,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(toArray(list), ['vscode', '.build', 'github', 'build.js', 'build']); }); - test('recursive filter with collapse', function () { + test('recursive filter with collapse', () => { const list: ITreeNode[] = []; let query = new RegExp(''); const filter = new class implements ITreeFilter { @@ -582,7 +699,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(toArray(list), ['vscode']); }); - test('recursive filter while collapsed', function () { + test('recursive filter while collapsed', () => { const list: ITreeNode[] = []; let query = new RegExp(''); const filter = new class implements ITreeFilter { @@ -635,9 +752,9 @@ suite('IndexTreeModel', function () { assert.deepEqual(list.length, 10); }); - suite('getNodeLocation', function () { + suite('getNodeLocation', () => { - test('simple', function () { + test('simple', () => { const list: IIndexTreeNode[] = []; const model = new IndexTreeModel('test', toList(list), -1); @@ -661,7 +778,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(model.getNodeLocation(list[5]), [2]); }); - test('with filter', function () { + test('with filter', () => { const list: IIndexTreeNode[] = []; const filter = new class implements ITreeFilter { filter(element: number): TreeVisibility { @@ -692,7 +809,7 @@ suite('IndexTreeModel', function () { }); }); - test('refilter with filtered out nodes', function () { + test('refilter with filtered out nodes', () => { const list: ITreeNode[] = []; let query = new RegExp(''); const filter = new class implements ITreeFilter { @@ -726,7 +843,7 @@ suite('IndexTreeModel', function () { assert.deepEqual(toArray(list), ['platinum']); }); - test('explicit hidden nodes should have renderNodeCount == 0, issue #83211', function () { + test('explicit hidden nodes should have renderNodeCount == 0, issue #83211', () => { const list: ITreeNode[] = []; let query = new RegExp(''); const filter = new class implements ITreeFilter { diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index ea333c0748..7579b17db4 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -59,30 +59,30 @@ suite('ObjectTree', function () { const navigator = tree.navigate(); - assert.equal(navigator.current(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.current(), 0); - assert.equal(navigator.next(), 10); - assert.equal(navigator.current(), 10); - assert.equal(navigator.next(), 11); - assert.equal(navigator.current(), 11); - assert.equal(navigator.next(), 12); - assert.equal(navigator.current(), 12); - assert.equal(navigator.next(), 1); - assert.equal(navigator.current(), 1); - assert.equal(navigator.next(), 2); - assert.equal(navigator.current(), 2); - assert.equal(navigator.previous(), 1); - assert.equal(navigator.current(), 1); - assert.equal(navigator.previous(), 12); - assert.equal(navigator.previous(), 11); - assert.equal(navigator.previous(), 10); - assert.equal(navigator.previous(), 0); - assert.equal(navigator.previous(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.next(), 10); - assert.equal(navigator.first(), 0); - assert.equal(navigator.last(), 2); + assert.strictEqual(navigator.current(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.current(), 0); + assert.strictEqual(navigator.next(), 10); + assert.strictEqual(navigator.current(), 10); + assert.strictEqual(navigator.next(), 11); + assert.strictEqual(navigator.current(), 11); + assert.strictEqual(navigator.next(), 12); + assert.strictEqual(navigator.current(), 12); + assert.strictEqual(navigator.next(), 1); + assert.strictEqual(navigator.current(), 1); + assert.strictEqual(navigator.next(), 2); + assert.strictEqual(navigator.current(), 2); + assert.strictEqual(navigator.previous(), 1); + assert.strictEqual(navigator.current(), 1); + assert.strictEqual(navigator.previous(), 12); + assert.strictEqual(navigator.previous(), 11); + assert.strictEqual(navigator.previous(), 10); + assert.strictEqual(navigator.previous(), 0); + assert.strictEqual(navigator.previous(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.next(), 10); + assert.strictEqual(navigator.first(), 0); + assert.strictEqual(navigator.last(), 2); }); test('should skip collapsed nodes', () => { @@ -100,18 +100,18 @@ suite('ObjectTree', function () { const navigator = tree.navigate(); - assert.equal(navigator.current(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.next(), 1); - assert.equal(navigator.next(), 2); - assert.equal(navigator.next(), null); - assert.equal(navigator.previous(), 2); - assert.equal(navigator.previous(), 1); - assert.equal(navigator.previous(), 0); - assert.equal(navigator.previous(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.first(), 0); - assert.equal(navigator.last(), 2); + assert.strictEqual(navigator.current(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.next(), 1); + assert.strictEqual(navigator.next(), 2); + assert.strictEqual(navigator.next(), null); + assert.strictEqual(navigator.previous(), 2); + assert.strictEqual(navigator.previous(), 1); + assert.strictEqual(navigator.previous(), 0); + assert.strictEqual(navigator.previous(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.first(), 0); + assert.strictEqual(navigator.last(), 2); }); test('should skip filtered elements', () => { @@ -131,21 +131,21 @@ suite('ObjectTree', function () { const navigator = tree.navigate(); - assert.equal(navigator.current(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.next(), 10); - assert.equal(navigator.next(), 12); - assert.equal(navigator.next(), 2); - assert.equal(navigator.next(), null); - assert.equal(navigator.previous(), 2); - assert.equal(navigator.previous(), 12); - assert.equal(navigator.previous(), 10); - assert.equal(navigator.previous(), 0); - assert.equal(navigator.previous(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.next(), 10); - assert.equal(navigator.first(), 0); - assert.equal(navigator.last(), 2); + assert.strictEqual(navigator.current(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.next(), 10); + assert.strictEqual(navigator.next(), 12); + assert.strictEqual(navigator.next(), 2); + assert.strictEqual(navigator.next(), null); + assert.strictEqual(navigator.previous(), 2); + assert.strictEqual(navigator.previous(), 12); + assert.strictEqual(navigator.previous(), 10); + assert.strictEqual(navigator.previous(), 0); + assert.strictEqual(navigator.previous(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.next(), 10); + assert.strictEqual(navigator.first(), 0); + assert.strictEqual(navigator.last(), 2); }); test('should be able to start from node', () => { @@ -163,20 +163,20 @@ suite('ObjectTree', function () { const navigator = tree.navigate(1); - assert.equal(navigator.current(), 1); - assert.equal(navigator.next(), 2); - assert.equal(navigator.current(), 2); - assert.equal(navigator.previous(), 1); - assert.equal(navigator.current(), 1); - assert.equal(navigator.previous(), 12); - assert.equal(navigator.previous(), 11); - assert.equal(navigator.previous(), 10); - assert.equal(navigator.previous(), 0); - assert.equal(navigator.previous(), null); - assert.equal(navigator.next(), 0); - assert.equal(navigator.next(), 10); - assert.equal(navigator.first(), 0); - assert.equal(navigator.last(), 2); + assert.strictEqual(navigator.current(), 1); + assert.strictEqual(navigator.next(), 2); + assert.strictEqual(navigator.current(), 2); + assert.strictEqual(navigator.previous(), 1); + assert.strictEqual(navigator.current(), 1); + assert.strictEqual(navigator.previous(), 12); + assert.strictEqual(navigator.previous(), 11); + assert.strictEqual(navigator.previous(), 10); + assert.strictEqual(navigator.previous(), 0); + assert.strictEqual(navigator.previous(), null); + assert.strictEqual(navigator.next(), 0); + assert.strictEqual(navigator.next(), 10); + assert.strictEqual(navigator.first(), 0); + assert.strictEqual(navigator.last(), 2); }); }); @@ -219,10 +219,10 @@ suite('ObjectTree', function () { }); }); -function toArray(list: NodeList): Node[] { - const result: Node[] = []; - list.forEach(node => result.push(node)); - return result; +function getRowsTextContent(container: HTMLElement): string[] { + const rows = [...container.querySelectorAll('.monaco-list-row')]; + rows.sort((a, b) => parseInt(a.getAttribute('data-index')!) - parseInt(b.getAttribute('data-index')!)); + return rows.map(row => row.querySelector('.monaco-tl-contents')!.textContent!); } suite('CompressibleObjectTree', function () { @@ -254,8 +254,7 @@ suite('CompressibleObjectTree', function () { const tree = new CompressibleObjectTree('test', container, new Delegate(), [new Renderer()]); tree.layout(200); - const rows = toArray(container.querySelectorAll('.monaco-tl-contents')); - assert.equal(rows.length, 0); + assert.strictEqual(getRowsTextContent(container).length, 0); }); test('simple', function () { @@ -278,8 +277,7 @@ suite('CompressibleObjectTree', function () { { element: 2 } ]); - const rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['0', '10', '11', '12', '1', '2']); + assert.deepStrictEqual(getRowsTextContent(container), ['0', '10', '11', '12', '1', '2']); }); test('compressed', () => { @@ -304,8 +302,7 @@ suite('CompressibleObjectTree', function () { } ]); - let rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11/111', '1111', '1112', '1113']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11/111', '1111', '1112', '1113']); tree.setChildren(11, [ { element: 111 }, @@ -313,30 +310,26 @@ suite('CompressibleObjectTree', function () { { element: 113 }, ]); - rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11', '111', '112', '113']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11', '111', '112', '113']); tree.setChildren(113, [ { element: 1131 } ]); - rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11', '111', '112', '113/1131']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11', '111', '112', '113/1131']); tree.setChildren(1131, [ { element: 1132 } ]); - rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11', '111', '112', '113/1131/1132']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11', '111', '112', '113/1131/1132']); tree.setChildren(1131, [ { element: 1132 }, { element: 1133 }, ]); - rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11', '111', '112', '113/1131', '1132', '1133']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11', '111', '112', '113/1131', '1132', '1133']); }); test('enableCompression', () => { @@ -361,15 +354,12 @@ suite('CompressibleObjectTree', function () { } ]); - let rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11/111', '1111', '1112', '1113']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11/111', '1111', '1112', '1113']); tree.updateOptions({ compressionEnabled: false }); - rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1', '11', '111', '1111', '1112', '1113']); + assert.deepStrictEqual(getRowsTextContent(container), ['1', '11', '111', '1111', '1112', '1113']); tree.updateOptions({ compressionEnabled: true }); - rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); - assert.deepEqual(rows, ['1/11/111', '1111', '1112', '1113']); + assert.deepStrictEqual(getRowsTextContent(container), ['1/11/111', '1111', '1112', '1113']); }); }); diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index 02d78eb21a..8bd582e2fd 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -10,25 +10,25 @@ suite('Arrays', () => { const array = [1, 4, 5, 7, 55, 59, 60, 61, 64, 69]; let idx = arrays.findFirstInSorted(array, e => e >= 0); - assert.equal(array[idx], 1); + assert.strictEqual(array[idx], 1); idx = arrays.findFirstInSorted(array, e => e > 1); - assert.equal(array[idx], 4); + assert.strictEqual(array[idx], 4); idx = arrays.findFirstInSorted(array, e => e >= 8); - assert.equal(array[idx], 55); + assert.strictEqual(array[idx], 55); idx = arrays.findFirstInSorted(array, e => e >= 61); - assert.equal(array[idx], 61); + assert.strictEqual(array[idx], 61); idx = arrays.findFirstInSorted(array, e => e >= 69); - assert.equal(array[idx], 69); + assert.strictEqual(array[idx], 69); idx = arrays.findFirstInSorted(array, e => e >= 70); - assert.equal(idx, array.length); + assert.strictEqual(idx, array.length); idx = arrays.findFirstInSorted([], e => e >= 0); - assert.equal(array[idx], 1); + assert.strictEqual(array[idx], 1); }); test('quickSelect', () => { @@ -36,10 +36,10 @@ suite('Arrays', () => { function assertMedian(expexted: number, data: number[], nth: number = Math.floor(data.length / 2)) { const compare = (a: number, b: number) => a - b; let actual1 = arrays.quickSelect(nth, data, compare); - assert.equal(actual1, expexted); + assert.strictEqual(actual1, expexted); let actual2 = data.slice().sort(compare)[nth]; - assert.equal(actual2, expexted); + assert.strictEqual(actual2, expexted); } assertMedian(5, [9, 1, 0, 2, 3, 4, 6, 8, 7, 10, 5]); @@ -72,18 +72,18 @@ suite('Arrays', () => { test('mergeSort', () => { let data = arrays.mergeSort([6, 5, 3, 1, 8, 7, 2, 4], (a, b) => a - b); - assert.deepEqual(data, [1, 2, 3, 4, 5, 6, 7, 8]); + assert.deepStrictEqual(data, [1, 2, 3, 4, 5, 6, 7, 8]); }); test('mergeSort, sorted array', function () { let data = arrays.mergeSort([1, 2, 3, 4, 5, 6], (a, b) => a - b); - assert.deepEqual(data, [1, 2, 3, 4, 5, 6]); + assert.deepStrictEqual(data, [1, 2, 3, 4, 5, 6]); }); test('mergeSort, is stable', function () { let numbers = arrays.mergeSort([33, 22, 11, 4, 99, 1], (a, b) => 0); - assert.deepEqual(numbers, [33, 22, 11, 4, 99, 1]); + assert.deepStrictEqual(numbers, [33, 22, 11, 4, 99, 1]); }); test('mergeSort, many random numbers', function () { @@ -129,40 +129,40 @@ suite('Arrays', () => { } let d = arrays.sortedDiff([1, 2, 4], [], compare); - assert.deepEqual(d, [ + assert.deepStrictEqual(d, [ { start: 0, deleteCount: 3, toInsert: [] } ]); d = arrays.sortedDiff([], [1, 2, 4], compare); - assert.deepEqual(d, [ + assert.deepStrictEqual(d, [ { start: 0, deleteCount: 0, toInsert: [1, 2, 4] } ]); d = arrays.sortedDiff([1, 2, 4], [1, 2, 4], compare); - assert.deepEqual(d, []); + assert.deepStrictEqual(d, []); d = arrays.sortedDiff([1, 2, 4], [2, 3, 4, 5], compare); - assert.deepEqual(d, [ + assert.deepStrictEqual(d, [ { start: 0, deleteCount: 1, toInsert: [] }, { start: 2, deleteCount: 0, toInsert: [3] }, { start: 3, deleteCount: 0, toInsert: [5] }, ]); d = arrays.sortedDiff([2, 3, 4, 5], [1, 2, 4], compare); - assert.deepEqual(d, [ + assert.deepStrictEqual(d, [ { start: 0, deleteCount: 0, toInsert: [1] }, { start: 1, deleteCount: 1, toInsert: [] }, { start: 3, deleteCount: 1, toInsert: [] }, ]); d = arrays.sortedDiff([1, 3, 5, 7], [5, 9, 11], compare); - assert.deepEqual(d, [ + assert.deepStrictEqual(d, [ { start: 0, deleteCount: 2, toInsert: [] }, { start: 3, deleteCount: 1, toInsert: [9, 11] } ]); d = arrays.sortedDiff([1, 3, 7], [5, 9, 11], compare); - assert.deepEqual(d, [ + assert.deepStrictEqual(d, [ { start: 0, deleteCount: 3, toInsert: [5, 9, 11] } ]); }); @@ -173,32 +173,32 @@ suite('Arrays', () => { } let d = arrays.delta([1, 2, 4], [], compare); - assert.deepEqual(d.removed, [1, 2, 4]); - assert.deepEqual(d.added, []); + assert.deepStrictEqual(d.removed, [1, 2, 4]); + assert.deepStrictEqual(d.added, []); d = arrays.delta([], [1, 2, 4], compare); - assert.deepEqual(d.removed, []); - assert.deepEqual(d.added, [1, 2, 4]); + assert.deepStrictEqual(d.removed, []); + assert.deepStrictEqual(d.added, [1, 2, 4]); d = arrays.delta([1, 2, 4], [1, 2, 4], compare); - assert.deepEqual(d.removed, []); - assert.deepEqual(d.added, []); + assert.deepStrictEqual(d.removed, []); + assert.deepStrictEqual(d.added, []); d = arrays.delta([1, 2, 4], [2, 3, 4, 5], compare); - assert.deepEqual(d.removed, [1]); - assert.deepEqual(d.added, [3, 5]); + assert.deepStrictEqual(d.removed, [1]); + assert.deepStrictEqual(d.added, [3, 5]); d = arrays.delta([2, 3, 4, 5], [1, 2, 4], compare); - assert.deepEqual(d.removed, [3, 5]); - assert.deepEqual(d.added, [1]); + assert.deepStrictEqual(d.removed, [3, 5]); + assert.deepStrictEqual(d.added, [1]); d = arrays.delta([1, 3, 5, 7], [5, 9, 11], compare); - assert.deepEqual(d.removed, [1, 3, 7]); - assert.deepEqual(d.added, [9, 11]); + assert.deepStrictEqual(d.removed, [1, 3, 7]); + assert.deepStrictEqual(d.added, [9, 11]); d = arrays.delta([1, 3, 7], [5, 9, 11], compare); - assert.deepEqual(d.removed, [1, 3, 7]); - assert.deepEqual(d.added, [5, 9, 11]); + assert.deepStrictEqual(d.removed, [1, 3, 7]); + assert.deepStrictEqual(d.added, [5, 9, 11]); }); test('binarySearch', () => { @@ -207,13 +207,13 @@ suite('Arrays', () => { } const array = [1, 4, 5, 7, 55, 59, 60, 61, 64, 69]; - assert.equal(arrays.binarySearch(array, 1, compare), 0); - assert.equal(arrays.binarySearch(array, 5, compare), 2); + assert.strictEqual(arrays.binarySearch(array, 1, compare), 0); + assert.strictEqual(arrays.binarySearch(array, 5, compare), 2); // insertion point - assert.equal(arrays.binarySearch(array, 0, compare), ~0); - assert.equal(arrays.binarySearch(array, 6, compare), ~3); - assert.equal(arrays.binarySearch(array, 70, compare), ~10); + assert.strictEqual(arrays.binarySearch(array, 0, compare), ~0); + assert.strictEqual(arrays.binarySearch(array, 6, compare), ~3); + assert.strictEqual(arrays.binarySearch(array, 70, compare), ~10); }); @@ -222,11 +222,11 @@ suite('Arrays', () => { return a; } - assert.deepEqual(arrays.distinct(['32', '4', '5'], compare), ['32', '4', '5']); - assert.deepEqual(arrays.distinct(['32', '4', '5', '4'], compare), ['32', '4', '5']); - assert.deepEqual(arrays.distinct(['32', 'constructor', '5', '1'], compare), ['32', 'constructor', '5', '1']); - assert.deepEqual(arrays.distinct(['32', 'constructor', 'proto', 'proto', 'constructor'], compare), ['32', 'constructor', 'proto']); - assert.deepEqual(arrays.distinct(['32', '4', '5', '32', '4', '5', '32', '4', '5', '5'], compare), ['32', '4', '5']); + assert.deepStrictEqual(arrays.distinct(['32', '4', '5'], compare), ['32', '4', '5']); + assert.deepStrictEqual(arrays.distinct(['32', '4', '5', '4'], compare), ['32', '4', '5']); + assert.deepStrictEqual(arrays.distinct(['32', 'constructor', '5', '1'], compare), ['32', 'constructor', '5', '1']); + assert.deepStrictEqual(arrays.distinct(['32', 'constructor', 'proto', 'proto', 'constructor'], compare), ['32', 'constructor', 'proto']); + assert.deepStrictEqual(arrays.distinct(['32', '4', '5', '32', '4', '5', '32', '4', '5', '5'], compare), ['32', '4', '5']); }); test('top', () => { @@ -236,13 +236,13 @@ suite('Arrays', () => { return a - b; }; - assert.deepEqual(arrays.top([], cmp, 1), []); - assert.deepEqual(arrays.top([1], cmp, 0), []); - assert.deepEqual(arrays.top([1, 2], cmp, 1), [1]); - assert.deepEqual(arrays.top([2, 1], cmp, 1), [1]); - assert.deepEqual(arrays.top([1, 3, 2], cmp, 2), [1, 2]); - assert.deepEqual(arrays.top([3, 2, 1], cmp, 3), [1, 2, 3]); - assert.deepEqual(arrays.top([4, 6, 2, 7, 8, 3, 5, 1], cmp, 3), [1, 2, 3]); + assert.deepStrictEqual(arrays.top([], cmp, 1), []); + assert.deepStrictEqual(arrays.top([1], cmp, 0), []); + assert.deepStrictEqual(arrays.top([1, 2], cmp, 1), [1]); + assert.deepStrictEqual(arrays.top([2, 1], cmp, 1), [1]); + assert.deepStrictEqual(arrays.top([1, 3, 2], cmp, 2), [1, 2]); + assert.deepStrictEqual(arrays.top([3, 2, 1], cmp, 3), [1, 2, 3]); + assert.deepStrictEqual(arrays.top([4, 6, 2, 7, 8, 3, 5, 1], cmp, 3), [1, 2, 3]); }); test('topAsync', async () => { @@ -259,56 +259,56 @@ suite('Arrays', () => { async function testTopAsync(cmp: any, m: number) { { const result = await arrays.topAsync([], cmp, 1, m); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); } { const result = await arrays.topAsync([1], cmp, 0, m); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); } { const result = await arrays.topAsync([1, 2], cmp, 1, m); - assert.deepEqual(result, [1]); + assert.deepStrictEqual(result, [1]); } { const result = await arrays.topAsync([2, 1], cmp, 1, m); - assert.deepEqual(result, [1]); + assert.deepStrictEqual(result, [1]); } { const result = await arrays.topAsync([1, 3, 2], cmp, 2, m); - assert.deepEqual(result, [1, 2]); + assert.deepStrictEqual(result, [1, 2]); } { const result = await arrays.topAsync([3, 2, 1], cmp, 3, m); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); } { const result = await arrays.topAsync([4, 6, 2, 7, 8, 3, 5, 1], cmp, 3, m); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); } } test('coalesce', () => { let a: Array = arrays.coalesce([null, 1, null, 2, 3]); - assert.equal(a.length, 3); - assert.equal(a[0], 1); - assert.equal(a[1], 2); - assert.equal(a[2], 3); + assert.strictEqual(a.length, 3); + assert.strictEqual(a[0], 1); + assert.strictEqual(a[1], 2); + assert.strictEqual(a[2], 3); arrays.coalesce([null, 1, null, undefined, undefined, 2, 3]); - assert.equal(a.length, 3); - assert.equal(a[0], 1); - assert.equal(a[1], 2); - assert.equal(a[2], 3); + assert.strictEqual(a.length, 3); + assert.strictEqual(a[0], 1); + assert.strictEqual(a[1], 2); + assert.strictEqual(a[2], 3); let b: number[] = []; b[10] = 1; b[20] = 2; b[30] = 3; b = arrays.coalesce(b); - assert.equal(b.length, 3); - assert.equal(b[0], 1); - assert.equal(b[1], 2); - assert.equal(b[2], 3); + assert.strictEqual(b.length, 3); + assert.strictEqual(b[0], 1); + assert.strictEqual(b[1], 2); + assert.strictEqual(b[2], 3); let sparse: number[] = []; sparse[0] = 1; @@ -317,36 +317,36 @@ suite('Arrays', () => { sparse[1000] = 1; sparse[1001] = 1; - assert.equal(sparse.length, 1002); + assert.strictEqual(sparse.length, 1002); sparse = arrays.coalesce(sparse); - assert.equal(sparse.length, 5); + assert.strictEqual(sparse.length, 5); }); test('coalesce - inplace', function () { let a: Array = [null, 1, null, 2, 3]; arrays.coalesceInPlace(a); - assert.equal(a.length, 3); - assert.equal(a[0], 1); - assert.equal(a[1], 2); - assert.equal(a[2], 3); + assert.strictEqual(a.length, 3); + assert.strictEqual(a[0], 1); + assert.strictEqual(a[1], 2); + assert.strictEqual(a[2], 3); a = [null, 1, null, undefined!, undefined!, 2, 3]; arrays.coalesceInPlace(a); - assert.equal(a.length, 3); - assert.equal(a[0], 1); - assert.equal(a[1], 2); - assert.equal(a[2], 3); + assert.strictEqual(a.length, 3); + assert.strictEqual(a[0], 1); + assert.strictEqual(a[1], 2); + assert.strictEqual(a[2], 3); let b: number[] = []; b[10] = 1; b[20] = 2; b[30] = 3; arrays.coalesceInPlace(b); - assert.equal(b.length, 3); - assert.equal(b[0], 1); - assert.equal(b[1], 2); - assert.equal(b[2], 3); + assert.strictEqual(b.length, 3); + assert.strictEqual(b[0], 1); + assert.strictEqual(b[1], 2); + assert.strictEqual(b[2], 3); let sparse: number[] = []; sparse[0] = 1; @@ -355,19 +355,19 @@ suite('Arrays', () => { sparse[1000] = 1; sparse[1001] = 1; - assert.equal(sparse.length, 1002); + assert.strictEqual(sparse.length, 1002); arrays.coalesceInPlace(sparse); - assert.equal(sparse.length, 5); + assert.strictEqual(sparse.length, 5); }); test('insert, remove', function () { const array: string[] = []; const remove = arrays.insert(array, 'foo'); - assert.equal(array[0], 'foo'); + assert.strictEqual(array[0], 'foo'); remove(); - assert.equal(array.length, 0); + assert.strictEqual(array.length, 0); }); test('splice', function () { diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 2866211b5c..46cf8c2d0d 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -18,7 +18,7 @@ suite('Async', () => { return new Promise(resolve => { /*never*/ }); }); let result = promise.then(_ => assert.ok(false), err => { - assert.equal(canceled, 1); + assert.strictEqual(canceled, 1); assert.ok(isPromiseCanceledError(err)); }); promise.cancel(); @@ -33,7 +33,7 @@ suite('Async', () => { return Promise.resolve(1234); }); let result = promise.then(_ => assert.ok(false), err => { - assert.equal(canceled, 1); + assert.strictEqual(canceled, 1); assert.ok(isPromiseCanceledError(err)); }); promise.cancel(); @@ -60,7 +60,7 @@ suite('Async', () => { cancellablePromise.cancel(); order.push('afterCancel'); - return promise.then(() => assert.deepEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); + return promise.then(() => assert.deepStrictEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); }); // Cancelling an async cancelable promise is just the same as a sync cancellable promise. @@ -82,7 +82,7 @@ suite('Async', () => { cancellablePromise.cancel(); order.push('afterCancel'); - return promise.then(() => assert.deepEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); + return promise.then(() => assert.deepStrictEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); }); test('cancelablePromise - get inner result', async function () { @@ -91,7 +91,7 @@ suite('Async', () => { }); let result = await promise; - assert.equal(result, 1234); + assert.strictEqual(result, 1234); }); test('Throttler - non async', function () { @@ -103,12 +103,12 @@ suite('Async', () => { let throttler = new async.Throttler(); return Promise.all([ - throttler.queue(factory).then((result) => { assert.equal(result, 1); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }) - ]).then(() => assert.equal(count, 2)); + throttler.queue(factory).then((result) => { assert.strictEqual(result, 1); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }) + ]).then(() => assert.strictEqual(count, 2)); }); test('Throttler', () => { @@ -118,18 +118,18 @@ suite('Async', () => { let throttler = new async.Throttler(); return Promise.all([ - throttler.queue(factory).then((result) => { assert.equal(result, 1); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }), - throttler.queue(factory).then((result) => { assert.equal(result, 2); }) + throttler.queue(factory).then((result) => { assert.strictEqual(result, 1); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 2); }) ]).then(() => { return Promise.all([ - throttler.queue(factory).then((result) => { assert.equal(result, 3); }), - throttler.queue(factory).then((result) => { assert.equal(result, 4); }), - throttler.queue(factory).then((result) => { assert.equal(result, 4); }), - throttler.queue(factory).then((result) => { assert.equal(result, 4); }), - throttler.queue(factory).then((result) => { assert.equal(result, 4); }) + throttler.queue(factory).then((result) => { assert.strictEqual(result, 3); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 4); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 4); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 4); }), + throttler.queue(factory).then((result) => { assert.strictEqual(result, 4); }) ]); }); }); @@ -143,9 +143,9 @@ suite('Async', () => { let promises: Promise[] = []; - promises.push(throttler.queue(factoryFactory(1)).then((n) => { assert.equal(n, 1); })); - promises.push(throttler.queue(factoryFactory(2)).then((n) => { assert.equal(n, 3); })); - promises.push(throttler.queue(factoryFactory(3)).then((n) => { assert.equal(n, 3); })); + promises.push(throttler.queue(factoryFactory(1)).then((n) => { assert.strictEqual(n, 1); })); + promises.push(throttler.queue(factoryFactory(2)).then((n) => { assert.strictEqual(n, 3); })); + promises.push(throttler.queue(factoryFactory(3)).then((n) => { assert.strictEqual(n, 3); })); return Promise.all(promises); }); @@ -161,13 +161,13 @@ suite('Async', () => { assert(!delayer.isTriggered()); - promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()); })); + promises.push(delayer.trigger(factory).then((result) => { assert.strictEqual(result, 1); assert(!delayer.isTriggered()); })); assert(delayer.isTriggered()); - promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()); })); + promises.push(delayer.trigger(factory).then((result) => { assert.strictEqual(result, 1); assert(!delayer.isTriggered()); })); assert(delayer.isTriggered()); - promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()); })); + promises.push(delayer.trigger(factory).then((result) => { assert.strictEqual(result, 1); assert(!delayer.isTriggered()); })); assert(delayer.isTriggered()); return Promise.all(promises).then(() => { @@ -237,7 +237,7 @@ suite('Async', () => { assert(!delayer.isTriggered()); const p = delayer.trigger(factory).then((result) => { - assert.equal(result, 1); + assert.strictEqual(result, 1); assert(!delayer.isTriggered()); promises.push(delayer.trigger(factory).then(undefined, () => { assert(true, 'yes, it was cancelled'); })); @@ -253,10 +253,10 @@ suite('Async', () => { assert(!delayer.isTriggered()); - promises.push(delayer.trigger(factory).then(() => { assert.equal(result, 1); assert(!delayer.isTriggered()); })); + promises.push(delayer.trigger(factory).then(() => { assert.strictEqual(result, 1); assert(!delayer.isTriggered()); })); assert(delayer.isTriggered()); - promises.push(delayer.trigger(factory).then(() => { assert.equal(result, 1); assert(!delayer.isTriggered()); })); + promises.push(delayer.trigger(factory).then(() => { assert.strictEqual(result, 1); assert(!delayer.isTriggered()); })); assert(delayer.isTriggered()); const p = Promise.all(promises).then(() => { @@ -286,9 +286,9 @@ suite('Async', () => { assert(!delayer.isTriggered()); - promises.push(delayer.trigger(factoryFactory(1)).then((n) => { assert.equal(n, 3); })); - promises.push(delayer.trigger(factoryFactory(2)).then((n) => { assert.equal(n, 3); })); - promises.push(delayer.trigger(factoryFactory(3)).then((n) => { assert.equal(n, 3); })); + promises.push(delayer.trigger(factoryFactory(1)).then((n) => { assert.strictEqual(n, 3); })); + promises.push(delayer.trigger(factoryFactory(2)).then((n) => { assert.strictEqual(n, 3); })); + promises.push(delayer.trigger(factoryFactory(3)).then((n) => { assert.strictEqual(n, 3); })); const p = Promise.all(promises).then(() => { assert(!delayer.isTriggered()); @@ -311,12 +311,12 @@ suite('Async', () => { factoryFactory(4), factoryFactory(5), ]).then((result) => { - assert.equal(5, result.length); - assert.equal(1, result[0]); - assert.equal(2, result[1]); - assert.equal(3, result[2]); - assert.equal(4, result[3]); - assert.equal(5, result[4]); + assert.strictEqual(5, result.length); + assert.strictEqual(1, result[0]); + assert.strictEqual(2, result[1]); + assert.strictEqual(3, result[2]); + assert.strictEqual(4, result[3]); + assert.strictEqual(5, result[4]); }); }); @@ -331,7 +331,7 @@ suite('Async', () => { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); return Promise.all(promises).then((res) => { - assert.equal(10, res.length); + assert.strictEqual(10, res.length); limiter = new async.Limiter(100); @@ -339,7 +339,7 @@ suite('Async', () => { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); return Promise.all(promises).then((res) => { - assert.equal(10, res.length); + assert.strictEqual(10, res.length); }); }); }); @@ -352,7 +352,7 @@ suite('Async', () => { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); return Promise.all(promises).then((res) => { - assert.equal(10, res.length); + assert.strictEqual(10, res.length); limiter = new async.Limiter(100); @@ -360,7 +360,7 @@ suite('Async', () => { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); return Promise.all(promises).then((res) => { - assert.equal(10, res.length); + assert.strictEqual(10, res.length); }); }); }); @@ -379,8 +379,8 @@ suite('Async', () => { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(n => promises.push(limiter.queue(factoryFactory(n)))); return Promise.all(promises).then((res) => { - assert.equal(10, res.length); - assert.deepEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], res); + assert.strictEqual(10, res.length); + assert.deepStrictEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], res); }); }); @@ -393,15 +393,15 @@ suite('Async', () => { let asyncPromise = false; let f2 = () => async.timeout(10).then(() => asyncPromise = true); - assert.equal(queue.size, 0); + assert.strictEqual(queue.size, 0); queue.queue(f1); - assert.equal(queue.size, 1); + assert.strictEqual(queue.size, 1); const p = queue.queue(f2); - assert.equal(queue.size, 2); + assert.strictEqual(queue.size, 2); return p.then(() => { - assert.equal(queue.size, 0); + assert.strictEqual(queue.size, 0); assert.ok(syncPromise); assert.ok(asyncPromise); }); @@ -423,11 +423,11 @@ suite('Async', () => { queue.queue(f3); queue.queue(f4); return queue.queue(f5).then(() => { - assert.equal(res[0], 1); - assert.equal(res[1], 2); - assert.equal(res[2], 3); - assert.equal(res[3], 4); - assert.equal(res[4], 5); + assert.strictEqual(res[0], 1); + assert.strictEqual(res[1], 2); + assert.strictEqual(res[2], 3); + assert.strictEqual(res[3], 4); + assert.strictEqual(res[4], 5); }); }); @@ -448,11 +448,11 @@ suite('Async', () => { queue.queue(f3).then(undefined, () => error = true); queue.queue(f4); return queue.queue(f5).then(() => { - assert.equal(res[0], 1); - assert.equal(res[1], 2); + assert.strictEqual(res[0], 1); + assert.strictEqual(res[1], 2); assert.ok(error); - assert.equal(res[2], 4); - assert.equal(res[3], 5); + assert.strictEqual(res[2], 4); + assert.strictEqual(res[3], 5); }); }); @@ -472,11 +472,11 @@ suite('Async', () => { return queue.queue(f3).then(() => { return queue.queue(f4).then(() => { return queue.queue(f5).then(() => { - assert.equal(res[0], 1); - assert.equal(res[1], 2); - assert.equal(res[2], 3); - assert.equal(res[3], 4); - assert.equal(res[4], 5); + assert.strictEqual(res[0], 1); + assert.strictEqual(res[1], 2); + assert.strictEqual(res[2], 3); + assert.strictEqual(res[3], 4); + assert.strictEqual(res[4], 5); }); }); }); @@ -521,7 +521,7 @@ suite('Async', () => { assert.ok(r1Queue); assert.ok(r2Queue); - assert.equal(r1Queue, queue.queueFor(URI.file('/some/path'))); // same queue returned + assert.strictEqual(r1Queue, queue.queueFor(URI.file('/some/path'))); // same queue returned let syncPromiseFactory = () => Promise.resolve(undefined); @@ -545,7 +545,7 @@ suite('Async', () => { return Promise.resolve(true); }, 10, 3); - assert.equal(res, true); + assert.strictEqual(res, true); }); test('retry - error case', async () => { @@ -555,12 +555,12 @@ suite('Async', () => { return Promise.reject(expectedError); }, 10, 3); } catch (error) { - assert.equal(error, error); + assert.strictEqual(error, error); } }); test('TaskSequentializer - pending basics', async function () { - const sequentializer = new async.TaskSequentializer(); + const sequentializer: any = new async.TaskSequentializer(); assert.ok(!sequentializer.hasPending()); assert.ok(!sequentializer.hasPending(2323)); @@ -576,12 +576,12 @@ suite('Async', () => { sequentializer.setPending(2, async.timeout(1)); assert.ok(sequentializer.hasPending()); assert.ok(sequentializer.hasPending(2)); - assert.ok(!sequentializer.hasPending(1)); + assert.strictEqual(sequentializer.hasPending(1), false); assert.ok(sequentializer.pending); await async.timeout(2); - assert.ok(!sequentializer.hasPending()); - assert.ok(!sequentializer.hasPending(2)); + assert.strictEqual(sequentializer.hasPending(), false); + assert.strictEqual(sequentializer.hasPending(2), false); assert.ok(!sequentializer.pending); }); @@ -673,7 +673,7 @@ suite('Async', () => { await p1; assert.ok(!triggered); - assert.equal(timedout, true); + assert.strictEqual(timedout, true); // promise wins timedout = false; @@ -684,38 +684,42 @@ suite('Async', () => { await p2; assert.ok(triggered); - assert.equal(timedout, false); + assert.strictEqual(timedout, false); }); test('SequencerByKey', async () => { const s = new async.SequencerByKey(); const r1 = await s.queue('key1', () => Promise.resolve('hello')); - assert.equal(r1, 'hello'); + assert.strictEqual(r1, 'hello'); await s.queue('key2', () => Promise.reject(new Error('failed'))).then(() => { throw new Error('should not be resolved'); }, err => { // Expected error - assert.equal(err.message, 'failed'); + assert.strictEqual(err.message, 'failed'); }); // Still works after a queued promise is rejected const r3 = await s.queue('key2', () => Promise.resolve('hello')); - assert.equal(r3, 'hello'); + assert.strictEqual(r3, 'hello'); }); test('IntervalCounter', async () => { const counter = new async.IntervalCounter(10); - assert.equal(counter.increment(), 1); - assert.equal(counter.increment(), 2); - assert.equal(counter.increment(), 3); + assert.strictEqual(counter.increment(), 1); + assert.strictEqual(counter.increment(), 2); + assert.strictEqual(counter.increment(), 3); + const now = Date.now(); await async.timeout(20); + if (Date.now() - now < 11) { + return; // Firefox in Playwright seems to have a flaky timeout implementation (https://github.com/microsoft/vscode/issues/114028) + } - assert.equal(counter.increment(), 1); - assert.equal(counter.increment(), 2); - assert.equal(counter.increment(), 3); + assert.strictEqual(counter.increment(), 1); + assert.strictEqual(counter.increment(), 2); + assert.strictEqual(counter.increment(), 3); }); test('firstParallel - simple', async () => { @@ -724,19 +728,19 @@ suite('Async', () => { Promise.resolve(2), Promise.resolve(3), ], v => v === 2); - assert.equal(a, 2); + assert.strictEqual(a, 2); }); test('firstParallel - uses null default', async () => { - assert.equal(await async.firstParallel([Promise.resolve(1)], v => v === 2), null); + assert.strictEqual(await async.firstParallel([Promise.resolve(1)], v => v === 2), null); }); test('firstParallel - uses value default', async () => { - assert.equal(await async.firstParallel([Promise.resolve(1)], v => v === 2, 4), 4); + assert.strictEqual(await async.firstParallel([Promise.resolve(1)], v => v === 2, 4), 4); }); test('firstParallel - empty', async () => { - assert.equal(await async.firstParallel([], v => v === 2, 4), 4); + assert.strictEqual(await async.firstParallel([], v => v === 2, 4), 4); }); test('firstParallel - cancels', async () => { @@ -753,9 +757,9 @@ suite('Async', () => { return 2; }); - assert.equal(await async.firstParallel([p1, p2], v => v === 2, 4), 2); - assert.equal(ct1!.isCancellationRequested, true, 'should cancel a'); - assert.equal(ct2!.isCancellationRequested, true, 'should cancel b'); + assert.strictEqual(await async.firstParallel([p1, p2], v => v === 2, 4), 2); + assert.strictEqual(ct1!.isCancellationRequested, true, 'should cancel a'); + assert.strictEqual(ct2!.isCancellationRequested, true, 'should cancel b'); }); test('firstParallel - rejection handling', async () => { @@ -772,8 +776,218 @@ suite('Async', () => { throw new Error('oh no'); }); - assert.equal(await async.firstParallel([p1, p2], v => v === 2, 4).catch(() => 'ok'), 'ok'); - assert.equal(ct1!.isCancellationRequested, true, 'should cancel a'); - assert.equal(ct2!.isCancellationRequested, true, 'should cancel b'); + assert.strictEqual(await async.firstParallel([p1, p2], v => v === 2, 4).catch(() => 'ok'), 'ok'); + assert.strictEqual(ct1!.isCancellationRequested, true, 'should cancel a'); + assert.strictEqual(ct2!.isCancellationRequested, true, 'should cancel b'); + }); + + suite('DeferredPromise', () => { + test('resolves', async () => { + const deferred = new async.DeferredPromise(); + assert.strictEqual(deferred.isResolved, false); + deferred.complete(42); + assert.strictEqual(await deferred.p, 42); + assert.strictEqual(deferred.isResolved, true); + }); + + test('rejects', async () => { + const deferred = new async.DeferredPromise(); + assert.strictEqual(deferred.isRejected, false); + const err = new Error('oh no!'); + deferred.error(err); + assert.strictEqual(await deferred.p.catch(e => e), err); + assert.strictEqual(deferred.isRejected, true); + }); + + test('cancels', async () => { + const deferred = new async.DeferredPromise(); + assert.strictEqual(deferred.isRejected, false); + deferred.cancel(); + assert.strictEqual((await deferred.p.catch(e => e)).name, 'Canceled'); + assert.strictEqual(deferred.isRejected, true); + }); + }); + + suite('Promises.allSettled', () => { + test('resolves', async () => { + const p1 = Promise.resolve(1); + const p2 = async.timeout(1).then(() => 2); + const p3 = async.timeout(2).then(() => 3); + + const result = await async.Promises.allSettled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { status: 'fulfilled', value: 1 }); + assert.deepStrictEqual(result[1], { status: 'fulfilled', value: 2 }); + assert.deepStrictEqual(result[2], { status: 'fulfilled', value: 3 }); + }); + + test('resolves in order', async () => { + const p1 = async.timeout(2).then(() => 1); + const p2 = async.timeout(1).then(() => 2); + const p3 = Promise.resolve(3); + + const result = await async.Promises.allSettled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { status: 'fulfilled', value: 1 }); + assert.deepStrictEqual(result[1], { status: 'fulfilled', value: 2 }); + assert.deepStrictEqual(result[2], { status: 'fulfilled', value: 3 }); + }); + + test('rejects', async () => { + const p1 = Promise.reject(1); + + const p2Error = new Error('2'); + const p2 = async.timeout(1).then(() => { throw p2Error; }); + + const p3Error = new Error('3'); + const p3 = async.timeout(2).then(() => { throw p3Error; }); + + const result = await async.Promises.allSettled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { status: 'rejected', reason: 1 }); + assert.deepStrictEqual(result[1], { status: 'rejected', reason: p2Error }); + assert.deepStrictEqual(result[2], { status: 'rejected', reason: p3Error }); + }); + + test('rejects in order', async () => { + const p1Error = new Error('1'); + const p1 = async.timeout(2).then(() => { throw p1Error; }); + + const p2Error = new Error('2'); + const p2 = async.timeout(1).then(() => { throw p2Error; }); + + const p3 = Promise.reject(3); + + const result = await async.Promises.allSettled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { status: 'rejected', reason: p1Error }); + assert.deepStrictEqual(result[1], { status: 'rejected', reason: p2Error }); + assert.deepStrictEqual(result[2], { status: 'rejected', reason: 3 }); + }); + + test('resolves & rejects', async () => { + const p1 = Promise.resolve(1); + const p2Error = new Error('2'); + const p2 = async.timeout(1).then(() => { throw p2Error; }); + const p3 = async.timeout(2).then(() => 3); + + const result = await async.Promises.allSettled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { status: 'fulfilled', value: 1 }); + assert.deepStrictEqual(result[1], { status: 'rejected', reason: p2Error }); + assert.deepStrictEqual(result[2], { status: 'fulfilled', value: 3 }); + }); + + test('resolves & rejects in order', async () => { + const p1Error = new Error('2'); + const p1 = async.timeout(1).then(() => { throw p1Error; }); + const p2 = async.timeout(2).then(() => 2); + const p3 = Promise.resolve(3); + + const result = await async.Promises.allSettled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], { status: 'rejected', reason: p1Error }); + assert.deepStrictEqual(result[1], { status: 'fulfilled', value: 2 }); + assert.deepStrictEqual(result[2], { status: 'fulfilled', value: 3 }); + }); + + test('can empty', async () => { + const result = await async.Promises.allSettled([]); + + assert.strictEqual(result.length, 0); + }); + }); + + suite('Promises.settled', () => { + test('resolves', async () => { + const p1 = Promise.resolve(1); + const p2 = async.timeout(1).then(() => 2); + const p3 = async.timeout(2).then(() => 3); + + const result = await async.Promises.settled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], 1); + assert.deepStrictEqual(result[1], 2); + assert.deepStrictEqual(result[2], 3); + }); + + test('resolves in order', async () => { + const p1 = async.timeout(2).then(() => 1); + const p2 = async.timeout(1).then(() => 2); + const p3 = Promise.resolve(3); + + const result = await async.Promises.settled([p1, p2, p3]); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result[0], 1); + assert.deepStrictEqual(result[1], 2); + assert.deepStrictEqual(result[2], 3); + }); + + test('rejects with first error but handles all promises (all errors)', async () => { + const p1 = Promise.reject(1); + + let p2Handled = false; + const p2Error = new Error('2'); + const p2 = async.timeout(1).then(() => { + p2Handled = true; + throw p2Error; + }); + + let p3Handled = false; + const p3Error = new Error('3'); + const p3 = async.timeout(2).then(() => { + p3Handled = true; + throw p3Error; + }); + + let error: Error | undefined = undefined; + try { + await async.Promises.settled([p1, p2, p3]); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.notStrictEqual(error, p2Error); + assert.notStrictEqual(error, p3Error); + assert.ok(p2Handled); + assert.ok(p3Handled); + }); + + test('rejects with first error but handles all promises (1 error)', async () => { + const p1 = Promise.resolve(1); + + let p2Handled = false; + const p2Error = new Error('2'); + const p2 = async.timeout(1).then(() => { + p2Handled = true; + throw p2Error; + }); + + let p3Handled = false; + const p3 = async.timeout(2).then(() => { + p3Handled = true; + return 3; + }); + + let error: Error | undefined = undefined; + try { + await async.Promises.settled([p1, p2, p3]); + } catch (e) { + error = e; + } + + assert.strictEqual(error, p2Error); + assert.ok(p2Handled); + assert.ok(p3Handled); + }); }); }); diff --git a/src/vs/base/test/node/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts similarity index 70% rename from src/vs/base/test/node/buffer.test.ts rename to src/vs/base/test/common/buffer.test.ts index 68a2c29255..b4d4ac77b5 100644 --- a/src/vs/base/test/node/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -13,28 +13,28 @@ suite('Buffer', () => { test('issue #71993 - VSBuffer#toString returns numbers', () => { const data = new Uint8Array([1, 2, 3, 'h'.charCodeAt(0), 'i'.charCodeAt(0), 4, 5]).buffer; const buffer = VSBuffer.wrap(new Uint8Array(data, 3, 2)); - assert.deepEqual(buffer.toString(), 'hi'); + assert.deepStrictEqual(buffer.toString(), 'hi'); }); test('bufferToReadable / readableToBuffer', () => { const content = 'Hello World'; const readable = bufferToReadable(VSBuffer.fromString(content)); - assert.equal(readableToBuffer(readable).toString(), content); + assert.strictEqual(readableToBuffer(readable).toString(), content); }); test('bufferToStream / streamToBuffer', async () => { const content = 'Hello World'; const stream = bufferToStream(VSBuffer.fromString(content)); - assert.equal((await streamToBuffer(stream)).toString(), content); + assert.strictEqual((await streamToBuffer(stream)).toString(), content); }); test('bufferedStreamToBuffer', async () => { const content = 'Hello World'; const stream = await peekStream(bufferToStream(VSBuffer.fromString(content)), 1); - assert.equal((await bufferedStreamToBuffer(stream)).toString(), content); + assert.strictEqual((await bufferedStreamToBuffer(stream)).toString(), content); }); test('bufferWriteableStream - basics (no error)', async () => { @@ -60,11 +60,11 @@ suite('Buffer', () => { await timeout(0); stream.end(VSBuffer.fromString('World')); - assert.equal(chunks.length, 2); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(chunks[1].toString(), 'World'); - assert.equal(ended, true); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 2); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(chunks[1].toString(), 'World'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 0); }); test('bufferWriteableStream - basics (error)', async () => { @@ -90,10 +90,10 @@ suite('Buffer', () => { await timeout(0); stream.end(new Error()); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(ended, true); - assert.equal(errors.length, 1); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 1); }); test('bufferWriteableStream - buffers data when no listener', async () => { @@ -119,10 +119,10 @@ suite('Buffer', () => { errors.push(error); }); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'HelloWorld'); - assert.equal(ended, true); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'HelloWorld'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 0); }); test('bufferWriteableStream - buffers errors when no listener', async () => { @@ -150,10 +150,10 @@ suite('Buffer', () => { stream.end(); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(ended, true); - assert.equal(errors.length, 1); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 1); }); test('bufferWriteableStream - buffers end when no listener', async () => { @@ -179,10 +179,10 @@ suite('Buffer', () => { errors.push(error); }); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'HelloWorld'); - assert.equal(ended, true); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'HelloWorld'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 0); }); test('bufferWriteableStream - nothing happens after end()', async () => { @@ -220,13 +220,13 @@ suite('Buffer', () => { await timeout(0); stream.end(VSBuffer.fromString('World')); - assert.equal(dataCalledAfterEnd, false); - assert.equal(errorCalledAfterEnd, false); - assert.equal(endCalledAfterEnd, false); + assert.strictEqual(dataCalledAfterEnd, false); + assert.strictEqual(errorCalledAfterEnd, false); + assert.strictEqual(endCalledAfterEnd, false); - assert.equal(chunks.length, 2); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(chunks[1].toString(), 'World'); + assert.strictEqual(chunks.length, 2); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(chunks[1].toString(), 'World'); }); test('bufferWriteableStream - pause/resume (simple)', async () => { @@ -254,16 +254,16 @@ suite('Buffer', () => { await timeout(0); stream.end(VSBuffer.fromString('World')); - assert.equal(chunks.length, 0); - assert.equal(errors.length, 0); - assert.equal(ended, false); + assert.strictEqual(chunks.length, 0); + assert.strictEqual(errors.length, 0); + assert.strictEqual(ended, false); stream.resume(); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'HelloWorld'); - assert.equal(ended, true); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'HelloWorld'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 0); }); test('bufferWriteableStream - pause/resume (pause after first write)', async () => { @@ -292,18 +292,18 @@ suite('Buffer', () => { await timeout(0); stream.end(VSBuffer.fromString('World')); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(errors.length, 0); - assert.equal(ended, false); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(errors.length, 0); + assert.strictEqual(ended, false); stream.resume(); - assert.equal(chunks.length, 2); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(chunks[1].toString(), 'World'); - assert.equal(ended, true); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 2); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(chunks[1].toString(), 'World'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 0); }); test('bufferWriteableStream - pause/resume (error)', async () => { @@ -331,16 +331,16 @@ suite('Buffer', () => { await timeout(0); stream.end(new Error()); - assert.equal(chunks.length, 0); - assert.equal(ended, false); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 0); + assert.strictEqual(ended, false); + assert.strictEqual(errors.length, 0); stream.resume(); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].toString(), 'Hello'); - assert.equal(ended, true); - assert.equal(errors.length, 1); + assert.strictEqual(chunks.length, 1); + assert.strictEqual(chunks[0].toString(), 'Hello'); + assert.strictEqual(ended, true); + assert.strictEqual(errors.length, 1); }); test('bufferWriteableStream - destroy', async () => { @@ -368,46 +368,46 @@ suite('Buffer', () => { await timeout(0); stream.end(VSBuffer.fromString('World')); - assert.equal(chunks.length, 0); - assert.equal(ended, false); - assert.equal(errors.length, 0); + assert.strictEqual(chunks.length, 0); + assert.strictEqual(ended, false); + assert.strictEqual(errors.length, 0); }); - test('Performance issue with VSBuffer#slice #76076', function () { + test('Performance issue with VSBuffer#slice #76076', function () { // TODO@alexdima this test seems to fail in web (https://github.com/microsoft/vscode/issues/114042) // Buffer#slice creates a view - { + if (typeof Buffer !== 'undefined') { const buff = Buffer.from([10, 20, 30, 40]); const b2 = buff.slice(1, 3); - assert.equal(buff[1], 20); - assert.equal(b2[0], 20); + assert.strictEqual(buff[1], 20); + assert.strictEqual(b2[0], 20); buff[1] = 17; // modify buff AND b2 - assert.equal(buff[1], 17); - assert.equal(b2[0], 17); + assert.strictEqual(buff[1], 17); + assert.strictEqual(b2[0], 17); } // TypedArray#slice creates a copy { const unit = new Uint8Array([10, 20, 30, 40]); const u2 = unit.slice(1, 3); - assert.equal(unit[1], 20); - assert.equal(u2[0], 20); + assert.strictEqual(unit[1], 20); + assert.strictEqual(u2[0], 20); unit[1] = 17; // modify unit, NOT b2 - assert.equal(unit[1], 17); - assert.equal(u2[0], 20); + assert.strictEqual(unit[1], 17); + assert.strictEqual(u2[0], 20); } // TypedArray#subarray creates a view { const unit = new Uint8Array([10, 20, 30, 40]); const u2 = unit.subarray(1, 3); - assert.equal(unit[1], 20); - assert.equal(u2[0], 20); + assert.strictEqual(unit[1], 20); + assert.strictEqual(u2[0], 20); unit[1] = 17; // modify unit AND b2 - assert.equal(unit[1], 17); - assert.equal(u2[0], 17); + assert.strictEqual(unit[1], 17); + assert.strictEqual(u2[0], 17); } }); }); diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 8c78270a47..ee09f70ce3 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -8,17 +8,17 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cance suite('CancellationToken', function () { test('None', () => { - assert.equal(CancellationToken.None.isCancellationRequested, false); - assert.equal(typeof CancellationToken.None.onCancellationRequested, 'function'); + assert.strictEqual(CancellationToken.None.isCancellationRequested, false); + assert.strictEqual(typeof CancellationToken.None.onCancellationRequested, 'function'); }); test('cancel before token', function (done) { const source = new CancellationTokenSource(); - assert.equal(source.token.isCancellationRequested, false); + assert.strictEqual(source.token.isCancellationRequested, false); source.cancel(); - assert.equal(source.token.isCancellationRequested, true); + assert.strictEqual(source.token.isCancellationRequested, true); source.token.onCancellationRequested(function () { assert.ok(true); @@ -29,7 +29,7 @@ suite('CancellationToken', function () { test('cancel happens only once', function () { let source = new CancellationTokenSource(); - assert.equal(source.token.isCancellationRequested, false); + assert.strictEqual(source.token.isCancellationRequested, false); let cancelCount = 0; function onCancel() { @@ -41,7 +41,7 @@ suite('CancellationToken', function () { source.cancel(); source.cancel(); - assert.equal(cancelCount, 1); + assert.strictEqual(cancelCount, 1); }); test('cancel calls all listeners', function () { @@ -60,7 +60,7 @@ suite('CancellationToken', function () { }); source.cancel(); - assert.equal(count, 3); + assert.strictEqual(count, 3); }); test('token stays the same', function () { @@ -92,7 +92,7 @@ suite('CancellationToken', function () { source.dispose(); source.cancel(); - assert.equal(count, 0); + assert.strictEqual(count, 0); }); test('dispose calls no listeners (unless told to cancel)', function () { @@ -106,7 +106,7 @@ suite('CancellationToken', function () { source.dispose(true); // source.cancel(); - assert.equal(count, 1); + assert.strictEqual(count, 1); }); test('parent cancels child', function () { @@ -119,8 +119,8 @@ suite('CancellationToken', function () { parent.cancel(); - assert.equal(count, 1); - assert.equal(child.token.isCancellationRequested, true); - assert.equal(parent.token.isCancellationRequested, true); + assert.strictEqual(count, 1); + assert.strictEqual(child.token.isCancellationRequested, true); + assert.strictEqual(parent.token.isCancellationRequested, true); }); }); diff --git a/src/vs/base/test/common/charCode.test.ts b/src/vs/base/test/common/charCode.test.ts index 1c2423413a..12620e05ce 100644 --- a/src/vs/base/test/common/charCode.test.ts +++ b/src/vs/base/test/common/charCode.test.ts @@ -10,7 +10,7 @@ suite('CharCode', () => { test('has good values', () => { function assertValue(actual: CharCode, expected: string): void { - assert.equal(actual, expected.charCodeAt(0), 'char code ok for <<' + expected + '>>'); + assert.strictEqual(actual, expected.charCodeAt(0), 'char code ok for <<' + expected + '>>'); } assertValue(CharCode.Tab, '\t'); diff --git a/src/vs/base/test/node/console.test.ts b/src/vs/base/test/common/console.test.ts similarity index 99% rename from src/vs/base/test/node/console.test.ts rename to src/vs/base/test/common/console.test.ts index cd8531d152..1b8f1f1080 100644 --- a/src/vs/base/test/node/console.test.ts +++ b/src/vs/base/test/common/console.test.ts @@ -45,4 +45,4 @@ suite('Console', () => { assert.equal(frame.line, 18); assert.equal(frame.column, 17); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/diff/diff.test.ts b/src/vs/base/test/common/diff/diff.test.ts index a098cdebf2..e2dd8ae51a 100644 --- a/src/vs/base/test/common/diff/diff.test.ts +++ b/src/vs/base/test/common/diff/diff.test.ts @@ -49,11 +49,11 @@ function assertAnswer(originalStr: string, modifiedStr: string, changes: IDiffCh let modifiedAnswer = maskBasedSubstring(modifiedStr, modifiedMask); if (onlyLength) { - assert.equal(originalAnswer.length, answerStr.length); - assert.equal(modifiedAnswer.length, answerStr.length); + assert.strictEqual(originalAnswer.length, answerStr.length); + assert.strictEqual(modifiedAnswer.length, answerStr.length); } else { - assert.equal(originalAnswer, answerStr); - assert.equal(modifiedAnswer, answerStr); + assert.strictEqual(originalAnswer, answerStr); + assert.strictEqual(modifiedAnswer, answerStr); } } @@ -106,18 +106,18 @@ suite('Diff - Ported from VS', () => { let predicateCallCount = 0; let diff = new LcsDiff(new StringDiffSequence(left), new StringDiffSequence(right), function (leftIndex, longestMatchSoFar) { - assert.equal(predicateCallCount, 0); + assert.strictEqual(predicateCallCount, 0); predicateCallCount++; - assert.equal(leftIndex, 1); + assert.strictEqual(leftIndex, 1); // cancel processing return false; }); let changes = diff.ComputeDiff(true).changes; - assert.equal(predicateCallCount, 1); + assert.strictEqual(predicateCallCount, 1); // Doesn't include 'c', 'd', or 'e', since we quit on the first request assertAnswer(left, right, changes, 'abf'); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index b71ac81b5a..b8dfde8dbe 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Event, Emitter, EventBufferer, EventMultiplexer, IWaitUntil, PauseableEmitter, AsyncEmitter } from 'vs/base/common/event'; +import { Event, Emitter, EventBufferer, EventMultiplexer, PauseableEmitter } from 'vs/base/common/event'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import * as Errors from 'vs/base/common/errors'; -import { timeout } from 'vs/base/common/async'; +import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { AsyncEmitter, IWaitUntil, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; namespace Samples { @@ -57,7 +57,7 @@ suite('Event', function () { // unhook listener subscription.dispose(); doc.setText('boo'); - assert.equal(counter.count, 2); + assert.strictEqual(counter.count, 2); }); @@ -80,7 +80,7 @@ suite('Event', function () { subscription.dispose(); doc.setText('boo'); - assert.equal(counter.count, 2); + assert.strictEqual(counter.count, 2); }); test('Emitter, store', function () { @@ -100,7 +100,7 @@ suite('Event', function () { subscription.dispose(); doc.setText('boo'); - assert.equal(counter.count, 2); + assert.strictEqual(counter.count, 2); }); test('onFirstAdd|onLastRemove', () => { @@ -112,25 +112,25 @@ suite('Event', function () { onLastListenerRemove() { lastCount += 1; } }); - assert.equal(firstCount, 0); - assert.equal(lastCount, 0); + assert.strictEqual(firstCount, 0); + assert.strictEqual(lastCount, 0); let subscription = a.event(function () { }); - assert.equal(firstCount, 1); - assert.equal(lastCount, 0); + assert.strictEqual(firstCount, 1); + assert.strictEqual(lastCount, 0); subscription.dispose(); - assert.equal(firstCount, 1); - assert.equal(lastCount, 1); + assert.strictEqual(firstCount, 1); + assert.strictEqual(lastCount, 1); subscription = a.event(function () { }); - assert.equal(firstCount, 2); - assert.equal(lastCount, 1); + assert.strictEqual(firstCount, 2); + assert.strictEqual(lastCount, 1); }); test('throwingListener', () => { - const origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => null); + const origErrorHandler = errorHandler.getUnexpectedErrorHandler(); + setUnexpectedErrorHandler(() => null); try { let a = new Emitter(); @@ -143,10 +143,10 @@ suite('Event', function () { hit = true; }); a.fire(undefined); - assert.equal(hit, true); + assert.strictEqual(hit, true); } finally { - Errors.setUnexpectedErrorHandler(origErrorHandler); + setUnexpectedErrorHandler(origErrorHandler); } }); @@ -162,15 +162,15 @@ suite('Event', function () { let reg2 = emitter.event(listener, context); emitter.fire(undefined); - assert.equal(counter, 2); + assert.strictEqual(counter, 2); reg1.dispose(); emitter.fire(undefined); - assert.equal(counter, 3); + assert.strictEqual(counter, 3); reg2.dispose(); emitter.fire(undefined); - assert.equal(counter, 3); + assert.strictEqual(counter, 3); }); test('Debounce Event', function (done: () => void) { @@ -192,9 +192,9 @@ suite('Event', function () { assert.ok(keys, 'was not expecting keys.'); if (count === 1) { doc.setText('4'); - assert.deepEqual(keys, ['1', '2', '3']); + assert.deepStrictEqual(keys, ['1', '2', '3']); } else if (count === 2) { - assert.deepEqual(keys, ['4']); + assert.deepStrictEqual(keys, ['4']); done(); } }); @@ -217,7 +217,7 @@ suite('Event', function () { emitter.fire(); await timeout(1); - assert.equal(calls, 1); + assert.strictEqual(calls, 1); }); test('Debounce Event - leading', async function () { @@ -234,7 +234,7 @@ suite('Event', function () { emitter.fire(); emitter.fire(); await timeout(1); - assert.equal(calls, 2); + assert.strictEqual(calls, 2); }); test('Debounce Event - leading reset', async function () { @@ -248,7 +248,7 @@ suite('Event', function () { emitter.fire(1); await timeout(1); - assert.deepEqual(calls, [1, 1]); + assert.deepStrictEqual(calls, [1, 1]); }); test('Emitter - In Order Delivery', function () { @@ -258,7 +258,7 @@ suite('Event', function () { if (event === 'e1') { a.fire('e2'); // assert that all events are delivered at this point - assert.deepEqual(listener2Events, ['e1', 'e2']); + assert.deepStrictEqual(listener2Events, ['e1', 'e2']); } }); a.event(function listener2(event) { @@ -267,7 +267,7 @@ suite('Event', function () { a.fire('e1'); // assert that all events are delivered in order - assert.deepEqual(listener2Events, ['e1', 'e2']); + assert.deepStrictEqual(listener2Events, ['e1', 'e2']); }); }); @@ -283,9 +283,9 @@ suite('AsyncEmitter', function () { let emitter = new AsyncEmitter(); emitter.event(e => { - assert.equal(e.foo, true); - assert.equal(e.bar, 1); - assert.equal(typeof e.waitUntil, 'function'); + assert.strictEqual(e.foo, true); + assert.strictEqual(e.bar, 1); + assert.strictEqual(typeof e.waitUntil, 'function'); }); emitter.fireAsync({ foo: true, bar: 1, }, CancellationToken.None); @@ -303,20 +303,20 @@ suite('AsyncEmitter', function () { emitter.event(e => { e.waitUntil(timeout(10).then(_ => { - assert.equal(globalState, 0); + assert.strictEqual(globalState, 0); globalState += 1; })); }); emitter.event(e => { e.waitUntil(timeout(1).then(_ => { - assert.equal(globalState, 1); + assert.strictEqual(globalState, 1); globalState += 1; })); }); await emitter.fireAsync({ foo: true }, CancellationToken.None); - assert.equal(globalState, 2); + assert.strictEqual(globalState, 2); }); test('sequential, in-order delivery', async function () { @@ -332,7 +332,7 @@ suite('AsyncEmitter', function () { e.waitUntil(timeout(10).then(async _ => { if (e.foo === 1) { await emitter.fireAsync({ foo: 2 }, CancellationToken.None); - assert.deepEqual(events, [1, 2]); + assert.deepStrictEqual(events, [1, 2]); done = true; } })); @@ -349,8 +349,8 @@ suite('AsyncEmitter', function () { }); test('catch errors', async function () { - const origErrorHandler = Errors.errorHandler.getUnexpectedErrorHandler(); - Errors.setUnexpectedErrorHandler(() => null); + const origErrorHandler = errorHandler.getUnexpectedErrorHandler(); + setUnexpectedErrorHandler(() => null); interface E extends IWaitUntil { foo: boolean; @@ -367,16 +367,17 @@ suite('AsyncEmitter', function () { emitter.event(e => { globalState += 1; e.waitUntil(timeout(10)); + e.waitUntil(timeout(20).then(() => globalState++)); // multiple `waitUntil` are supported and awaited on }); await emitter.fireAsync({ foo: true }, CancellationToken.None).then(() => { - assert.equal(globalState, 2); + assert.strictEqual(globalState, 3); }).catch(e => { console.log(e); assert.ok(false); }); - Errors.setUnexpectedErrorHandler(origErrorHandler); + setUnexpectedErrorHandler(origErrorHandler); }); }); @@ -390,7 +391,7 @@ suite('PausableEmitter', function () { emitter.fire(1); emitter.fire(2); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); }); test('pause/resume - no merge', function () { @@ -400,17 +401,17 @@ suite('PausableEmitter', function () { emitter.event(e => data.push(e)); emitter.fire(1); emitter.fire(2); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.pause(); emitter.fire(3); emitter.fire(4); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.resume(); - assert.deepEqual(data, [1, 2, 3, 4]); + assert.deepStrictEqual(data, [1, 2, 3, 4]); emitter.fire(5); - assert.deepEqual(data, [1, 2, 3, 4, 5]); + assert.deepStrictEqual(data, [1, 2, 3, 4, 5]); }); test('pause/resume - merge', function () { @@ -420,18 +421,18 @@ suite('PausableEmitter', function () { emitter.event(e => data.push(e)); emitter.fire(1); emitter.fire(2); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.pause(); emitter.fire(3); emitter.fire(4); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.resume(); - assert.deepEqual(data, [1, 2, 7]); + assert.deepStrictEqual(data, [1, 2, 7]); emitter.fire(5); - assert.deepEqual(data, [1, 2, 7, 5]); + assert.deepStrictEqual(data, [1, 2, 7, 5]); }); test('double pause/resume', function () { @@ -441,22 +442,22 @@ suite('PausableEmitter', function () { emitter.event(e => data.push(e)); emitter.fire(1); emitter.fire(2); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.pause(); emitter.pause(); emitter.fire(3); emitter.fire(4); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.resume(); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.resume(); - assert.deepEqual(data, [1, 2, 3, 4]); + assert.deepStrictEqual(data, [1, 2, 3, 4]); emitter.resume(); - assert.deepEqual(data, [1, 2, 3, 4]); + assert.deepStrictEqual(data, [1, 2, 3, 4]); }); test('resume, no pause', function () { @@ -466,11 +467,11 @@ suite('PausableEmitter', function () { emitter.event(e => data.push(e)); emitter.fire(1); emitter.fire(2); - assert.deepEqual(data, [1, 2]); + assert.deepStrictEqual(data, [1, 2]); emitter.resume(); emitter.fire(3); - assert.deepEqual(data, [1, 2, 3]); + assert.deepStrictEqual(data, [1, 2, 3]); }); test('nested pause', function () { @@ -493,16 +494,16 @@ suite('PausableEmitter', function () { emitter.pause(); emitter.fire(1); emitter.fire(2); - assert.deepEqual(data, []); + assert.deepStrictEqual(data, []); emitter.resume(); - assert.deepEqual(data, [1, 1]); // paused after first event + assert.deepStrictEqual(data, [1, 1]); // paused after first event emitter.resume(); - assert.deepEqual(data, [1, 1, 2, 2]); // remaing event delivered + assert.deepStrictEqual(data, [1, 1, 2, 2]); // remaing event delivered emitter.fire(3); - assert.deepEqual(data, [1, 1, 2, 2, 3, 3]); + assert.deepStrictEqual(data, [1, 1, 2, 2, 3, 3]); }); }); @@ -518,13 +519,13 @@ suite('Event utils', () => { const event = bufferer.wrapEvent(emitter.event); const listener = event(counter.onEvent, counter); - assert.equal(counter.count, 0); + assert.strictEqual(counter.count, 0); emitter.fire(); - assert.equal(counter.count, 1); + assert.strictEqual(counter.count, 1); emitter.fire(); - assert.equal(counter.count, 2); + assert.strictEqual(counter.count, 2); emitter.fire(); - assert.equal(counter.count, 3); + assert.strictEqual(counter.count, 3); listener.dispose(); }); @@ -536,20 +537,20 @@ suite('Event utils', () => { const event = bufferer.wrapEvent(emitter.event); const listener = event(counter.onEvent, counter); - assert.equal(counter.count, 0); + assert.strictEqual(counter.count, 0); emitter.fire(); - assert.equal(counter.count, 1); + assert.strictEqual(counter.count, 1); bufferer.bufferEvents(() => { emitter.fire(); - assert.equal(counter.count, 1); + assert.strictEqual(counter.count, 1); emitter.fire(); - assert.equal(counter.count, 1); + assert.strictEqual(counter.count, 1); }); - assert.equal(counter.count, 3); + assert.strictEqual(counter.count, 3); emitter.fire(); - assert.equal(counter.count, 4); + assert.strictEqual(counter.count, 4); listener.dispose(); }); @@ -563,20 +564,20 @@ suite('Event utils', () => { const listener2 = Event.once(emitter.event)(() => counter2++); const listener3 = Event.once(emitter.event)(() => counter3++); - assert.equal(counter1, 0); - assert.equal(counter2, 0); - assert.equal(counter3, 0); + assert.strictEqual(counter1, 0); + assert.strictEqual(counter2, 0); + assert.strictEqual(counter3, 0); listener3.dispose(); emitter.fire(); - assert.equal(counter1, 1); - assert.equal(counter2, 1); - assert.equal(counter3, 0); + assert.strictEqual(counter1, 1); + assert.strictEqual(counter2, 1); + assert.strictEqual(counter3, 0); emitter.fire(); - assert.equal(counter1, 2); - assert.equal(counter2, 1); - assert.equal(counter3, 0); + assert.strictEqual(counter1, 2); + assert.strictEqual(counter2, 1); + assert.strictEqual(counter3, 0); listener1.dispose(); listener2.dispose(); @@ -591,10 +592,10 @@ suite('Event utils', () => { const event = Event.fromPromise(Promise.resolve(null)); event(() => count++); - assert.equal(count, 0); + assert.strictEqual(count, 0); await timeout(10); - assert.equal(count, 1); + assert.strictEqual(count, 1); }); test('should emit when done - setTimeout', async () => { @@ -604,9 +605,9 @@ suite('Event utils', () => { const event = Event.fromPromise(promise); event(() => count++); - assert.equal(count, 0); + assert.strictEqual(count, 0); await promise; - assert.equal(count, 1); + assert.strictEqual(count, 1); }); }); @@ -646,14 +647,14 @@ suite('Event utils', () => { assert.deepEqual(result, []); const listener = bufferedEvent(num => result.push(num)); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); emitter.fire(4); - assert.deepEqual(result, [1, 2, 3, 4]); + assert.deepStrictEqual(result, [1, 2, 3, 4]); listener.dispose(); emitter.fire(5); - assert.deepEqual(result, [1, 2, 3, 4]); + assert.deepStrictEqual(result, [1, 2, 3, 4]); }); test('should buffer events on next tick', async () => { @@ -668,14 +669,14 @@ suite('Event utils', () => { assert.deepEqual(result, []); const listener = bufferedEvent(num => result.push(num)); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); await timeout(10); emitter.fire(4); - assert.deepEqual(result, [1, 2, 3, 4]); + assert.deepStrictEqual(result, [1, 2, 3, 4]); listener.dispose(); emitter.fire(5); - assert.deepEqual(result, [1, 2, 3, 4]); + assert.deepStrictEqual(result, [1, 2, 3, 4]); }); test('should fire initial buffer events', () => { @@ -690,7 +691,7 @@ suite('Event utils', () => { assert.deepEqual(result, []); bufferedEvent(num => result.push(num)); - assert.deepEqual(result, [-2, -1, 0, 1, 2, 3]); + assert.deepStrictEqual(result, [-2, -1, 0, 1, 2, 3]); }); }); @@ -704,10 +705,10 @@ suite('Event utils', () => { const e1 = new Emitter(); m.add(e1.event); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); }); test('multiplexer dispose works', () => { @@ -718,16 +719,16 @@ suite('Event utils', () => { const e1 = new Emitter(); m.add(e1.event); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); m.dispose(); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); }); test('event dispose works', () => { @@ -738,16 +739,16 @@ suite('Event utils', () => { const e1 = new Emitter(); m.add(e1.event); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); e1.dispose(); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); }); test('mutliplexer event dispose works', () => { @@ -758,16 +759,16 @@ suite('Event utils', () => { const e1 = new Emitter(); const l1 = m.add(e1.event); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); l1.dispose(); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); e1.fire(0); - assert.deepEqual(result, [0]); + assert.deepStrictEqual(result, [0]); }); test('hot start works', () => { @@ -785,7 +786,7 @@ suite('Event utils', () => { e1.fire(1); e2.fire(2); e3.fire(3); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); }); test('cold start works', () => { @@ -804,7 +805,7 @@ suite('Event utils', () => { e1.fire(1); e2.fire(2); e3.fire(3); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); }); test('late add works', () => { @@ -825,7 +826,7 @@ suite('Event utils', () => { m.add(e3.event); e3.fire(3); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); }); test('add dispose works', () => { @@ -845,15 +846,15 @@ suite('Event utils', () => { const e3 = new Emitter(); const l3 = m.add(e3.event); e3.fire(3); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); l3.dispose(); e3.fire(4); - assert.deepEqual(result, [1, 2, 3]); + assert.deepStrictEqual(result, [1, 2, 3]); e2.fire(4); e1.fire(5); - assert.deepEqual(result, [1, 2, 3, 4, 5]); + assert.deepStrictEqual(result, [1, 2, 3, 4, 5]); }); }); @@ -864,31 +865,31 @@ suite('Event utils', () => { const result: number[] = []; const listener = event(num => result.push(num)); - assert.deepEqual(result, []); + assert.deepStrictEqual(result, []); emitter.fire(1); - assert.deepEqual(result, [1]); + assert.deepStrictEqual(result, [1]); emitter.fire(2); - assert.deepEqual(result, [1, 2]); + assert.deepStrictEqual(result, [1, 2]); emitter.fire(2); - assert.deepEqual(result, [1, 2]); + assert.deepStrictEqual(result, [1, 2]); emitter.fire(1); - assert.deepEqual(result, [1, 2, 1]); + assert.deepStrictEqual(result, [1, 2, 1]); emitter.fire(1); - assert.deepEqual(result, [1, 2, 1]); + assert.deepStrictEqual(result, [1, 2, 1]); emitter.fire(3); - assert.deepEqual(result, [1, 2, 1, 3]); + assert.deepStrictEqual(result, [1, 2, 1, 3]); emitter.fire(3); - assert.deepEqual(result, [1, 2, 1, 3]); + assert.deepStrictEqual(result, [1, 2, 1, 3]); emitter.fire(3); - assert.deepEqual(result, [1, 2, 1, 3]); + assert.deepStrictEqual(result, [1, 2, 1, 3]); listener.dispose(); }); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index c295eebce0..6dfa5923b4 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -5,45 +5,43 @@ import * as assert from 'assert'; import * as extpath from 'vs/base/common/extpath'; -import * as platform from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; import { CharCode } from 'vs/base/common/charCode'; suite('Paths', () => { test('toForwardSlashes', () => { - assert.equal(extpath.toSlashes('\\\\server\\share\\some\\path'), '//server/share/some/path'); - assert.equal(extpath.toSlashes('c:\\test'), 'c:/test'); - assert.equal(extpath.toSlashes('foo\\bar'), 'foo/bar'); - assert.equal(extpath.toSlashes('/user/far'), '/user/far'); + assert.strictEqual(extpath.toSlashes('\\\\server\\share\\some\\path'), '//server/share/some/path'); + assert.strictEqual(extpath.toSlashes('c:\\test'), 'c:/test'); + assert.strictEqual(extpath.toSlashes('foo\\bar'), 'foo/bar'); + assert.strictEqual(extpath.toSlashes('/user/far'), '/user/far'); }); test('getRoot', () => { - assert.equal(extpath.getRoot('/user/far'), '/'); - assert.equal(extpath.getRoot('\\\\server\\share\\some\\path'), '//server/share/'); - assert.equal(extpath.getRoot('//server/share/some/path'), '//server/share/'); - assert.equal(extpath.getRoot('//server/share'), '/'); - assert.equal(extpath.getRoot('//server'), '/'); - assert.equal(extpath.getRoot('//server//'), '/'); - assert.equal(extpath.getRoot('c:/user/far'), 'c:/'); - assert.equal(extpath.getRoot('c:user/far'), 'c:'); - assert.equal(extpath.getRoot('http://www'), ''); - assert.equal(extpath.getRoot('http://www/'), 'http://www/'); - assert.equal(extpath.getRoot('file:///foo'), 'file:///'); - assert.equal(extpath.getRoot('file://foo'), ''); + assert.strictEqual(extpath.getRoot('/user/far'), '/'); + assert.strictEqual(extpath.getRoot('\\\\server\\share\\some\\path'), '//server/share/'); + assert.strictEqual(extpath.getRoot('//server/share/some/path'), '//server/share/'); + assert.strictEqual(extpath.getRoot('//server/share'), '/'); + assert.strictEqual(extpath.getRoot('//server'), '/'); + assert.strictEqual(extpath.getRoot('//server//'), '/'); + assert.strictEqual(extpath.getRoot('c:/user/far'), 'c:/'); + assert.strictEqual(extpath.getRoot('c:user/far'), 'c:'); + assert.strictEqual(extpath.getRoot('http://www'), ''); + assert.strictEqual(extpath.getRoot('http://www/'), 'http://www/'); + assert.strictEqual(extpath.getRoot('file:///foo'), 'file:///'); + assert.strictEqual(extpath.getRoot('file://foo'), ''); }); - test('isUNC', () => { - if (platform.isWindows) { - assert.ok(!extpath.isUNC('foo')); - assert.ok(!extpath.isUNC('/foo')); - assert.ok(!extpath.isUNC('\\foo')); - assert.ok(!extpath.isUNC('\\\\foo')); - assert.ok(extpath.isUNC('\\\\a\\b')); - assert.ok(!extpath.isUNC('//a/b')); - assert.ok(extpath.isUNC('\\\\server\\share')); - assert.ok(extpath.isUNC('\\\\server\\share\\')); - assert.ok(extpath.isUNC('\\\\server\\share\\path')); - } + (!isWindows ? test.skip : test)('isUNC', () => { + assert.ok(!extpath.isUNC('foo')); + assert.ok(!extpath.isUNC('/foo')); + assert.ok(!extpath.isUNC('\\foo')); + assert.ok(!extpath.isUNC('\\\\foo')); + assert.ok(extpath.isUNC('\\\\a\\b')); + assert.ok(!extpath.isUNC('//a/b')); + assert.ok(extpath.isUNC('\\\\server\\share')); + assert.ok(extpath.isUNC('\\\\server\\share\\')); + assert.ok(extpath.isUNC('\\\\server\\share\\path')); }); test('isValidBasename', () => { @@ -53,7 +51,7 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('/test.txt')); assert.ok(!extpath.isValidBasename('\\test.txt')); - if (platform.isWindows) { + if (isWindows) { assert.ok(!extpath.isValidBasename('aux')); assert.ok(!extpath.isValidBasename('Aux')); assert.ok(!extpath.isValidBasename('LPT0')); @@ -74,43 +72,43 @@ suite('Paths', () => { }); test('sanitizeFilePath', () => { - if (platform.isWindows) { - assert.equal(extpath.sanitizeFilePath('.', 'C:\\the\\cwd'), 'C:\\the\\cwd'); - assert.equal(extpath.sanitizeFilePath('', 'C:\\the\\cwd'), 'C:\\the\\cwd'); + if (isWindows) { + assert.strictEqual(extpath.sanitizeFilePath('.', 'C:\\the\\cwd'), 'C:\\the\\cwd'); + assert.strictEqual(extpath.sanitizeFilePath('', 'C:\\the\\cwd'), 'C:\\the\\cwd'); - assert.equal(extpath.sanitizeFilePath('C:', 'C:\\the\\cwd'), 'C:\\'); - assert.equal(extpath.sanitizeFilePath('C:\\', 'C:\\the\\cwd'), 'C:\\'); - assert.equal(extpath.sanitizeFilePath('C:\\\\', 'C:\\the\\cwd'), 'C:\\'); + assert.strictEqual(extpath.sanitizeFilePath('C:', 'C:\\the\\cwd'), 'C:\\'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\', 'C:\\the\\cwd'), 'C:\\'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\\\', 'C:\\the\\cwd'), 'C:\\'); - assert.equal(extpath.sanitizeFilePath('C:\\folder\\my.txt', 'C:\\the\\cwd'), 'C:\\folder\\my.txt'); - assert.equal(extpath.sanitizeFilePath('C:\\folder\\my', 'C:\\the\\cwd'), 'C:\\folder\\my'); - assert.equal(extpath.sanitizeFilePath('C:\\folder\\..\\my', 'C:\\the\\cwd'), 'C:\\my'); - assert.equal(extpath.sanitizeFilePath('C:\\folder\\my\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); - assert.equal(extpath.sanitizeFilePath('C:\\folder\\my\\\\\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\folder\\my.txt', 'C:\\the\\cwd'), 'C:\\folder\\my.txt'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\folder\\my', 'C:\\the\\cwd'), 'C:\\folder\\my'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\folder\\..\\my', 'C:\\the\\cwd'), 'C:\\my'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\folder\\my\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); + assert.strictEqual(extpath.sanitizeFilePath('C:\\folder\\my\\\\\\', 'C:\\the\\cwd'), 'C:\\folder\\my'); - assert.equal(extpath.sanitizeFilePath('my.txt', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); - assert.equal(extpath.sanitizeFilePath('my.txt\\', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); + assert.strictEqual(extpath.sanitizeFilePath('my.txt', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); + assert.strictEqual(extpath.sanitizeFilePath('my.txt\\', 'C:\\the\\cwd'), 'C:\\the\\cwd\\my.txt'); - assert.equal(extpath.sanitizeFilePath('\\\\localhost\\folder\\my', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); - assert.equal(extpath.sanitizeFilePath('\\\\localhost\\folder\\my\\', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); + assert.strictEqual(extpath.sanitizeFilePath('\\\\localhost\\folder\\my', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); + assert.strictEqual(extpath.sanitizeFilePath('\\\\localhost\\folder\\my\\', 'C:\\the\\cwd'), '\\\\localhost\\folder\\my'); } else { - assert.equal(extpath.sanitizeFilePath('.', '/the/cwd'), '/the/cwd'); - assert.equal(extpath.sanitizeFilePath('', '/the/cwd'), '/the/cwd'); - assert.equal(extpath.sanitizeFilePath('/', '/the/cwd'), '/'); + assert.strictEqual(extpath.sanitizeFilePath('.', '/the/cwd'), '/the/cwd'); + assert.strictEqual(extpath.sanitizeFilePath('', '/the/cwd'), '/the/cwd'); + assert.strictEqual(extpath.sanitizeFilePath('/', '/the/cwd'), '/'); - assert.equal(extpath.sanitizeFilePath('/folder/my.txt', '/the/cwd'), '/folder/my.txt'); - assert.equal(extpath.sanitizeFilePath('/folder/my', '/the/cwd'), '/folder/my'); - assert.equal(extpath.sanitizeFilePath('/folder/../my', '/the/cwd'), '/my'); - assert.equal(extpath.sanitizeFilePath('/folder/my/', '/the/cwd'), '/folder/my'); - assert.equal(extpath.sanitizeFilePath('/folder/my///', '/the/cwd'), '/folder/my'); + assert.strictEqual(extpath.sanitizeFilePath('/folder/my.txt', '/the/cwd'), '/folder/my.txt'); + assert.strictEqual(extpath.sanitizeFilePath('/folder/my', '/the/cwd'), '/folder/my'); + assert.strictEqual(extpath.sanitizeFilePath('/folder/../my', '/the/cwd'), '/my'); + assert.strictEqual(extpath.sanitizeFilePath('/folder/my/', '/the/cwd'), '/folder/my'); + assert.strictEqual(extpath.sanitizeFilePath('/folder/my///', '/the/cwd'), '/folder/my'); - assert.equal(extpath.sanitizeFilePath('my.txt', '/the/cwd'), '/the/cwd/my.txt'); - assert.equal(extpath.sanitizeFilePath('my.txt/', '/the/cwd'), '/the/cwd/my.txt'); + assert.strictEqual(extpath.sanitizeFilePath('my.txt', '/the/cwd'), '/the/cwd/my.txt'); + assert.strictEqual(extpath.sanitizeFilePath('my.txt/', '/the/cwd'), '/the/cwd/my.txt'); } }); - test('isRoot', () => { - if (platform.isWindows) { + test('isRootOrDriveLetter', () => { + if (isWindows) { assert.ok(extpath.isRootOrDriveLetter('c:')); assert.ok(extpath.isRootOrDriveLetter('D:')); assert.ok(extpath.isRootOrDriveLetter('D:/')); @@ -123,6 +121,34 @@ suite('Paths', () => { } }); + test('hasDriveLetter', () => { + if (isWindows) { + assert.ok(extpath.hasDriveLetter('c:')); + assert.ok(extpath.hasDriveLetter('D:')); + assert.ok(extpath.hasDriveLetter('D:/')); + assert.ok(extpath.hasDriveLetter('D:\\')); + assert.ok(extpath.hasDriveLetter('D:\\path')); + assert.ok(extpath.hasDriveLetter('D:/path')); + } else { + assert.ok(!extpath.hasDriveLetter('/')); + assert.ok(!extpath.hasDriveLetter('/path')); + } + }); + + test('getDriveLetter', () => { + if (isWindows) { + assert.strictEqual(extpath.getDriveLetter('c:'), 'c'); + assert.strictEqual(extpath.getDriveLetter('D:'), 'D'); + assert.strictEqual(extpath.getDriveLetter('D:/'), 'D'); + assert.strictEqual(extpath.getDriveLetter('D:\\'), 'D'); + assert.strictEqual(extpath.getDriveLetter('D:\\path'), 'D'); + assert.strictEqual(extpath.getDriveLetter('D:/path'), 'D'); + } else { + assert.ok(!extpath.getDriveLetter('/')); + assert.ok(!extpath.getDriveLetter('/path')); + } + }); + test('isWindowsDriveLetter', () => { assert.ok(!extpath.isWindowsDriveLetter(0)); assert.ok(!extpath.isWindowsDriveLetter(-1)); @@ -131,47 +157,47 @@ suite('Paths', () => { }); test('indexOfPath', () => { - assert.equal(extpath.indexOfPath('/foo', '/bar', true), -1); - assert.equal(extpath.indexOfPath('/foo', '/FOO', false), -1); - assert.equal(extpath.indexOfPath('/foo', '/FOO', true), 0); - assert.equal(extpath.indexOfPath('/some/long/path', '/some/long', false), 0); - assert.equal(extpath.indexOfPath('/some/long/path', '/PATH', true), 10); + assert.strictEqual(extpath.indexOfPath('/foo', '/bar', true), -1); + assert.strictEqual(extpath.indexOfPath('/foo', '/FOO', false), -1); + assert.strictEqual(extpath.indexOfPath('/foo', '/FOO', true), 0); + assert.strictEqual(extpath.indexOfPath('/some/long/path', '/some/long', false), 0); + assert.strictEqual(extpath.indexOfPath('/some/long/path', '/PATH', true), 10); }); test('parseLineAndColumnAware', () => { let res = extpath.parseLineAndColumnAware('/foo/bar'); - assert.equal(res.path, '/foo/bar'); - assert.equal(res.line, undefined); - assert.equal(res.column, undefined); + assert.strictEqual(res.path, '/foo/bar'); + assert.strictEqual(res.line, undefined); + assert.strictEqual(res.column, undefined); res = extpath.parseLineAndColumnAware('/foo/bar:33'); - assert.equal(res.path, '/foo/bar'); - assert.equal(res.line, 33); - assert.equal(res.column, 1); + assert.strictEqual(res.path, '/foo/bar'); + assert.strictEqual(res.line, 33); + assert.strictEqual(res.column, 1); res = extpath.parseLineAndColumnAware('/foo/bar:33:34'); - assert.equal(res.path, '/foo/bar'); - assert.equal(res.line, 33); - assert.equal(res.column, 34); + assert.strictEqual(res.path, '/foo/bar'); + assert.strictEqual(res.line, 33); + assert.strictEqual(res.column, 34); res = extpath.parseLineAndColumnAware('C:\\foo\\bar'); - assert.equal(res.path, 'C:\\foo\\bar'); - assert.equal(res.line, undefined); - assert.equal(res.column, undefined); + assert.strictEqual(res.path, 'C:\\foo\\bar'); + assert.strictEqual(res.line, undefined); + assert.strictEqual(res.column, undefined); res = extpath.parseLineAndColumnAware('C:\\foo\\bar:33'); - assert.equal(res.path, 'C:\\foo\\bar'); - assert.equal(res.line, 33); - assert.equal(res.column, 1); + assert.strictEqual(res.path, 'C:\\foo\\bar'); + assert.strictEqual(res.line, 33); + assert.strictEqual(res.column, 1); res = extpath.parseLineAndColumnAware('C:\\foo\\bar:33:34'); - assert.equal(res.path, 'C:\\foo\\bar'); - assert.equal(res.line, 33); - assert.equal(res.column, 34); + assert.strictEqual(res.path, 'C:\\foo\\bar'); + assert.strictEqual(res.line, 33); + assert.strictEqual(res.column, 34); res = extpath.parseLineAndColumnAware('/foo/bar:abb'); - assert.equal(res.path, '/foo/bar:abb'); - assert.equal(res.line, undefined); - assert.equal(res.column, undefined); + assert.strictEqual(res.path, '/foo/bar:abb'); + assert.strictEqual(res.line, undefined); + assert.strictEqual(res.column, undefined); }); }); diff --git a/src/vs/base/test/common/filters.perf.test.ts b/src/vs/base/test/common/filters.perf.test.ts index cd21e58ec0..391c6e88a3 100644 --- a/src/vs/base/test/common/filters.perf.test.ts +++ b/src/vs/base/test/common/filters.perf.test.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as filters from 'vs/base/common/filters'; -import { data } from './filters.perf.data'; +import { data } from 'vs/base/test/common/filters.perf.data'; const patterns = ['cci', 'ida', 'pos', 'CCI', 'enbled', 'callback', 'gGame', 'cons', 'zyx', 'aBc']; const _enablePerf = false; -function perfSuite(name: string, callback: (this: Mocha.ISuiteCallbackContext) => void) { +function perfSuite(name: string, callback: (this: Mocha.Suite) => void) { if (_enablePerf) { suite(name, callback); } @@ -17,6 +17,9 @@ function perfSuite(name: string, callback: (this: Mocha.ISuiteCallbackContext) = perfSuite('Performance - fuzzyMatch', function () { + // suiteSetup(() => console.profile()); + // suiteTeardown(() => console.profileEnd()); + console.log(`Matching ${data.length} items against ${patterns.length} patterns (${data.length * patterns.length} operations) `); function perfTest(name: string, match: filters.FuzzyScorer) { diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 10dbc2f192..55d16f09e4 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyScore, IMatch, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, createMatches } from 'vs/base/common/filters'; +import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyScore, IMatch, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, createMatches, anyScore } from 'vs/base/common/filters'; function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number; }[]) { let r = filter(word, wordToMatchAgainst); assert(r, `${word} didn't match ${wordToMatchAgainst}`); if (highlights) { - assert.deepEqual(r, highlights); + assert.deepStrictEqual(r, highlights); } } @@ -233,7 +233,7 @@ suite('Filters', () => { pos = match.end; } actualWord += word.substring(pos); - assert.equal(actualWord, decoratedWord); + assert.strictEqual(actualWord, decoratedWord); } } @@ -285,7 +285,7 @@ suite('Filters', () => { assertMatches('LLLL', 'SVisualLoggerLogsList', undefined, fuzzyScore); assertMatches('TEdit', 'TextEdit', '^Text^E^d^i^t', fuzzyScore); assertMatches('TEdit', 'TextEditor', '^Text^E^d^i^tor', fuzzyScore); - assertMatches('TEdit', 'Textedit', '^T^exte^d^i^t', fuzzyScore); + assertMatches('TEdit', 'Textedit', '^Text^e^d^i^t', fuzzyScore); assertMatches('TEdit', 'text_edit', '^text_^e^d^i^t', fuzzyScore); assertMatches('TEditDit', 'TextEditorDecorationType', '^Text^E^d^i^tor^Decorat^ion^Type', fuzzyScore); assertMatches('TEdit', 'TextEditorDecorationType', '^Text^E^d^i^torDecorationType', fuzzyScore); @@ -442,7 +442,7 @@ suite('Filters', () => { } } } - assert.equal(topIdx, expected, `${pattern} -> actual=${words[topIdx]} <> expected=${words[expected]}`); + assert.strictEqual(topIdx, expected, `${pattern} -> actual=${words[topIdx]} <> expected=${words[expected]}`); } test('topScore - fuzzyScore', function () { @@ -521,6 +521,12 @@ suite('Filters', () => { 'ffffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_^f^o^o', fuzzyScore ); + assertMatches( + 'Aoo', + 'Affffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', + '^Affffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_f^o^o', + fuzzyScore + ); assertMatches( 'foo', 'Gffffffffffffffffffffffffffffbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar_foo', @@ -534,8 +540,19 @@ suite('Filters', () => { assert.ok(Boolean(match)); }); + test('Wrong highlight after emoji #113404', function () { + assertMatches('di', '✨div classname="">', '✨^d^iv classname="">', fuzzyScore); + assertMatches('di', 'adiv classname="">', 'adiv classname="">', fuzzyScore); + }); + test('Suggestion is not highlighted #85826', function () { assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScore); assertMatches('SemanticTokens', 'SemanticTokensEdits', '^S^e^m^a^n^t^i^c^T^o^k^e^n^sEdits', fuzzyScoreGracefulAggressive); }); + + test('IntelliSense completion not correctly highlighting text in front of cursor #115250', function () { + assertMatches('lo', 'log', '^l^og', fuzzyScore); + assertMatches('.lo', 'log', '^l^og', anyScore); + assertMatches('.', 'log', 'log', anyScore); + }); }); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index e9dffd3edd..3dd6a1baaa 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as scorer from 'vs/base/common/fuzzyScorer'; +import { IItemAccessor, FuzzyScore, FuzzyScore2, IItemScore, prepareQuery, scoreFuzzy, scoreFuzzy2, scoreItemFuzzy, compareItemsByFuzzyScore, pieceToQuery } from 'vs/base/common/fuzzyScorer'; import { URI } from 'vs/base/common/uri'; import { basename, dirname, sep, posix, win32 } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; -class ResourceAccessorClass implements scorer.IItemAccessor { +class ResourceAccessorClass implements IItemAccessor { getItemLabel(resource: URI): string { return basename(resource.fsPath); @@ -27,7 +27,7 @@ class ResourceAccessorClass implements scorer.IItemAccessor { const ResourceAccessor = new ResourceAccessorClass(); -class ResourceWithSlashAccessorClass implements scorer.IItemAccessor { +class ResourceWithSlashAccessorClass implements IItemAccessor { getItemLabel(resource: URI): string { return basename(resource.fsPath); @@ -44,7 +44,7 @@ class ResourceWithSlashAccessorClass implements scorer.IItemAccessor { const ResourceWithSlashAccessor = new ResourceWithSlashAccessorClass(); -class ResourceWithBackslashAccessorClass implements scorer.IItemAccessor { +class ResourceWithBackslashAccessorClass implements IItemAccessor { getItemLabel(resource: URI): string { return basename(resource.fsPath); @@ -61,7 +61,7 @@ class ResourceWithBackslashAccessorClass implements scorer.IItemAccessor { const ResourceWithBackslashAccessor = new ResourceWithBackslashAccessorClass(); -class NullAccessorClass implements scorer.IItemAccessor { +class NullAccessorClass implements IItemAccessor { getItemLabel(resource: URI): string { return undefined!; @@ -76,24 +76,24 @@ class NullAccessorClass implements scorer.IItemAccessor { } } -function _doScore(target: string, query: string, fuzzy: boolean): scorer.FuzzyScore { - const preparedQuery = scorer.prepareQuery(query); +function _doScore(target: string, query: string, fuzzy: boolean): FuzzyScore { + const preparedQuery = prepareQuery(query); - return scorer.scoreFuzzy(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, fuzzy); + return scoreFuzzy(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, fuzzy); } -function _doScore2(target: string, query: string, matchOffset: number = 0): scorer.FuzzyScore2 { - const preparedQuery = scorer.prepareQuery(query); +function _doScore2(target: string, query: string, matchOffset: number = 0): FuzzyScore2 { + const preparedQuery = prepareQuery(query); - return scorer.scoreFuzzy2(target, preparedQuery, 0, matchOffset); + return scoreFuzzy2(target, preparedQuery, 0, matchOffset); } -function scoreItem(item: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor): scorer.IItemScore { - return scorer.scoreItemFuzzy(item, scorer.prepareQuery(query), fuzzy, accessor, Object.create(null)); +function scoreItem(item: T, query: string, fuzzy: boolean, accessor: IItemAccessor): IItemScore { + return scoreItemFuzzy(item, prepareQuery(query), fuzzy, accessor, Object.create(null)); } -function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor): number { - return scorer.compareItemsByFuzzyScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, Object.create(null)); +function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: IItemAccessor): number { + return compareItemsByFuzzyScore(itemA, itemB, prepareQuery(query), fuzzy, accessor, Object.create(null)); } const NullAccessor = new NullAccessorClass(); @@ -103,7 +103,7 @@ suite('Fuzzy Scorer', () => { test('score (fuzzy)', function () { const target = 'HeLlo-World'; - const scores: scorer.FuzzyScore[] = []; + const scores: FuzzyScore[] = []; scores.push(_doScore(target, 'HelLo-World', true)); // direct case match scores.push(_doScore(target, 'hello-world', true)); // direct mix-case match scores.push(_doScore(target, 'HW', true)); // direct case prefix (multiple) @@ -120,30 +120,30 @@ suite('Fuzzy Scorer', () => { // Assert scoring order let sortedScores = scores.concat().sort((a, b) => b[0] - a[0]); - assert.deepEqual(scores, sortedScores); + assert.deepStrictEqual(scores, sortedScores); // Assert scoring positions // let positions = scores[0][1]; - // assert.equal(positions.length, 'HelLo-World'.length); + // assert.strictEqual(positions.length, 'HelLo-World'.length); // positions = scores[2][1]; - // assert.equal(positions.length, 'HW'.length); - // assert.equal(positions[0], 0); - // assert.equal(positions[1], 6); + // assert.strictEqual(positions.length, 'HW'.length); + // assert.strictEqual(positions[0], 0); + // assert.strictEqual(positions[1], 6); }); test('score (non fuzzy)', function () { const target = 'HeLlo-World'; assert.ok(_doScore(target, 'HelLo-World', false)[0] > 0); - assert.equal(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); + assert.strictEqual(_doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); assert.ok(_doScore(target, 'hello-world', false)[0] > 0); - assert.equal(_doScore(target, 'HW', false)[0], 0); + assert.strictEqual(_doScore(target, 'HW', false)[0], 0); assert.ok(_doScore(target, 'h', false)[0] > 0); assert.ok(_doScore(target, 'ello', false)[0] > 0); assert.ok(_doScore(target, 'ld', false)[0] > 0); - assert.equal(_doScore(target, 'eo', false)[0], 0); + assert.strictEqual(_doScore(target, 'eo', false)[0], 0); }); test('scoreItem - matches are proper', function () { @@ -158,52 +158,52 @@ suite('Fuzzy Scorer', () => { // Path Identity const identityRes = scoreItem(resource, ResourceAccessor.getItemPath(resource), true, ResourceAccessor); assert.ok(identityRes.score); - assert.equal(identityRes.descriptionMatch!.length, 1); - assert.equal(identityRes.labelMatch!.length, 1); - assert.equal(identityRes.descriptionMatch![0].start, 0); - assert.equal(identityRes.descriptionMatch![0].end, ResourceAccessor.getItemDescription(resource).length); - assert.equal(identityRes.labelMatch![0].start, 0); - assert.equal(identityRes.labelMatch![0].end, ResourceAccessor.getItemLabel(resource).length); + assert.strictEqual(identityRes.descriptionMatch!.length, 1); + assert.strictEqual(identityRes.labelMatch!.length, 1); + assert.strictEqual(identityRes.descriptionMatch![0].start, 0); + assert.strictEqual(identityRes.descriptionMatch![0].end, ResourceAccessor.getItemDescription(resource).length); + assert.strictEqual(identityRes.labelMatch![0].start, 0); + assert.strictEqual(identityRes.labelMatch![0].end, ResourceAccessor.getItemLabel(resource).length); // Basename Prefix const basenamePrefixRes = scoreItem(resource, 'som', true, ResourceAccessor); assert.ok(basenamePrefixRes.score); assert.ok(!basenamePrefixRes.descriptionMatch); - assert.equal(basenamePrefixRes.labelMatch!.length, 1); - assert.equal(basenamePrefixRes.labelMatch![0].start, 0); - assert.equal(basenamePrefixRes.labelMatch![0].end, 'som'.length); + assert.strictEqual(basenamePrefixRes.labelMatch!.length, 1); + assert.strictEqual(basenamePrefixRes.labelMatch![0].start, 0); + assert.strictEqual(basenamePrefixRes.labelMatch![0].end, 'som'.length); // Basename Camelcase const basenameCamelcaseRes = scoreItem(resource, 'sF', true, ResourceAccessor); assert.ok(basenameCamelcaseRes.score); assert.ok(!basenameCamelcaseRes.descriptionMatch); - assert.equal(basenameCamelcaseRes.labelMatch!.length, 2); - assert.equal(basenameCamelcaseRes.labelMatch![0].start, 0); - assert.equal(basenameCamelcaseRes.labelMatch![0].end, 1); - assert.equal(basenameCamelcaseRes.labelMatch![1].start, 4); - assert.equal(basenameCamelcaseRes.labelMatch![1].end, 5); + assert.strictEqual(basenameCamelcaseRes.labelMatch!.length, 2); + assert.strictEqual(basenameCamelcaseRes.labelMatch![0].start, 0); + assert.strictEqual(basenameCamelcaseRes.labelMatch![0].end, 1); + assert.strictEqual(basenameCamelcaseRes.labelMatch![1].start, 4); + assert.strictEqual(basenameCamelcaseRes.labelMatch![1].end, 5); // Basename Match const basenameRes = scoreItem(resource, 'of', true, ResourceAccessor); assert.ok(basenameRes.score); assert.ok(!basenameRes.descriptionMatch); - assert.equal(basenameRes.labelMatch!.length, 2); - assert.equal(basenameRes.labelMatch![0].start, 1); - assert.equal(basenameRes.labelMatch![0].end, 2); - assert.equal(basenameRes.labelMatch![1].start, 4); - assert.equal(basenameRes.labelMatch![1].end, 5); + assert.strictEqual(basenameRes.labelMatch!.length, 2); + assert.strictEqual(basenameRes.labelMatch![0].start, 1); + assert.strictEqual(basenameRes.labelMatch![0].end, 2); + assert.strictEqual(basenameRes.labelMatch![1].start, 4); + assert.strictEqual(basenameRes.labelMatch![1].end, 5); // Path Match const pathRes = scoreItem(resource, 'xyz123', true, ResourceAccessor); assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 8); - assert.equal(pathRes.labelMatch![0].end, 11); - assert.equal(pathRes.descriptionMatch!.length, 1); - assert.equal(pathRes.descriptionMatch![0].start, 1); - assert.equal(pathRes.descriptionMatch![0].end, 4); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 8); + assert.strictEqual(pathRes.labelMatch![0].end, 11); + assert.strictEqual(pathRes.descriptionMatch!.length, 1); + assert.strictEqual(pathRes.descriptionMatch![0].start, 1); + assert.strictEqual(pathRes.descriptionMatch![0].end, 4); // No Match const noRes = scoreItem(resource, '987', true, ResourceAccessor); @@ -223,51 +223,51 @@ suite('Fuzzy Scorer', () => { let res1 = scoreItem(resource, 'xyz some', true, ResourceAccessor); assert.ok(res1.score); - assert.equal(res1.labelMatch?.length, 1); - assert.equal(res1.labelMatch![0].start, 0); - assert.equal(res1.labelMatch![0].end, 4); - assert.equal(res1.descriptionMatch?.length, 1); - assert.equal(res1.descriptionMatch![0].start, 1); - assert.equal(res1.descriptionMatch![0].end, 4); + assert.strictEqual(res1.labelMatch?.length, 1); + assert.strictEqual(res1.labelMatch![0].start, 0); + assert.strictEqual(res1.labelMatch![0].end, 4); + assert.strictEqual(res1.descriptionMatch?.length, 1); + assert.strictEqual(res1.descriptionMatch![0].start, 1); + assert.strictEqual(res1.descriptionMatch![0].end, 4); let res2 = scoreItem(resource, 'some xyz', true, ResourceAccessor); assert.ok(res2.score); - assert.equal(res1.score, res2.score); - assert.equal(res2.labelMatch?.length, 1); - assert.equal(res2.labelMatch![0].start, 0); - assert.equal(res2.labelMatch![0].end, 4); - assert.equal(res2.descriptionMatch?.length, 1); - assert.equal(res2.descriptionMatch![0].start, 1); - assert.equal(res2.descriptionMatch![0].end, 4); + assert.strictEqual(res1.score, res2.score); + assert.strictEqual(res2.labelMatch?.length, 1); + assert.strictEqual(res2.labelMatch![0].start, 0); + assert.strictEqual(res2.labelMatch![0].end, 4); + assert.strictEqual(res2.descriptionMatch?.length, 1); + assert.strictEqual(res2.descriptionMatch![0].start, 1); + assert.strictEqual(res2.descriptionMatch![0].end, 4); let res3 = scoreItem(resource, 'some xyz file file123', true, ResourceAccessor); assert.ok(res3.score); assert.ok(res3.score > res2.score); - assert.equal(res3.labelMatch?.length, 1); - assert.equal(res3.labelMatch![0].start, 0); - assert.equal(res3.labelMatch![0].end, 11); - assert.equal(res3.descriptionMatch?.length, 1); - assert.equal(res3.descriptionMatch![0].start, 1); - assert.equal(res3.descriptionMatch![0].end, 4); + assert.strictEqual(res3.labelMatch?.length, 1); + assert.strictEqual(res3.labelMatch![0].start, 0); + assert.strictEqual(res3.labelMatch![0].end, 11); + assert.strictEqual(res3.descriptionMatch?.length, 1); + assert.strictEqual(res3.descriptionMatch![0].start, 1); + assert.strictEqual(res3.descriptionMatch![0].end, 4); let res4 = scoreItem(resource, 'path z y', true, ResourceAccessor); assert.ok(res4.score); assert.ok(res4.score < res2.score); - assert.equal(res4.labelMatch?.length, 0); - assert.equal(res4.descriptionMatch?.length, 2); - assert.equal(res4.descriptionMatch![0].start, 2); - assert.equal(res4.descriptionMatch![0].end, 4); - assert.equal(res4.descriptionMatch![1].start, 10); - assert.equal(res4.descriptionMatch![1].end, 14); + assert.strictEqual(res4.labelMatch?.length, 0); + assert.strictEqual(res4.descriptionMatch?.length, 2); + assert.strictEqual(res4.descriptionMatch![0].start, 2); + assert.strictEqual(res4.descriptionMatch![0].end, 4); + assert.strictEqual(res4.descriptionMatch![1].start, 10); + assert.strictEqual(res4.descriptionMatch![1].end, 14); }); test('scoreItem - invalid input', function () { let res = scoreItem(null, null!, true, ResourceAccessor); - assert.equal(res.score, 0); + assert.strictEqual(res.score, 0); res = scoreItem(null, 'null', true, ResourceAccessor); - assert.equal(res.score, 0); + assert.strictEqual(res.score, 0); }); test('scoreItem - optimize for file paths', function () { @@ -280,12 +280,12 @@ suite('Fuzzy Scorer', () => { assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 0); - assert.equal(pathRes.labelMatch![0].end, 7); - assert.equal(pathRes.descriptionMatch!.length, 1); - assert.equal(pathRes.descriptionMatch![0].start, 23); - assert.equal(pathRes.descriptionMatch![0].end, 26); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 0); + assert.strictEqual(pathRes.labelMatch![0].end, 7); + assert.strictEqual(pathRes.descriptionMatch!.length, 1); + assert.strictEqual(pathRes.descriptionMatch![0].start, 23); + assert.strictEqual(pathRes.descriptionMatch![0].end, 26); }); test('scoreItem - avoid match scattering (bug #36119)', function () { @@ -295,9 +295,9 @@ suite('Fuzzy Scorer', () => { assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); - assert.equal(pathRes.labelMatch!.length, 1); - assert.equal(pathRes.labelMatch![0].start, 0); - assert.equal(pathRes.labelMatch![0].end, 9); + assert.strictEqual(pathRes.labelMatch!.length, 1); + assert.strictEqual(pathRes.labelMatch![0].start, 0); + assert.strictEqual(pathRes.labelMatch![0].end, 9); }); test('scoreItem - prefers more compact matches', function () { @@ -309,11 +309,11 @@ suite('Fuzzy Scorer', () => { assert.ok(res.score); assert.ok(res.descriptionMatch); assert.ok(!res.labelMatch!.length); - assert.equal(res.descriptionMatch!.length, 2); - assert.equal(res.descriptionMatch![0].start, 11); - assert.equal(res.descriptionMatch![0].end, 12); - assert.equal(res.descriptionMatch![1].start, 13); - assert.equal(res.descriptionMatch![1].end, 14); + assert.strictEqual(res.descriptionMatch!.length, 2); + assert.strictEqual(res.descriptionMatch![0].start, 11); + assert.strictEqual(res.descriptionMatch![0].end, 12); + assert.strictEqual(res.descriptionMatch![1].start, 13); + assert.strictEqual(res.descriptionMatch![1].end, 14); }); test('scoreItem - proper target offset', function () { @@ -328,9 +328,9 @@ suite('Fuzzy Scorer', () => { const res = scoreItem(resource, 'de', true, ResourceAccessor); - assert.equal(res.labelMatch!.length, 1); - assert.equal(res.labelMatch![0].start, 1); - assert.equal(res.labelMatch![0].end, 3); + assert.strictEqual(res.labelMatch!.length, 1); + assert.strictEqual(res.labelMatch![0].start, 1); + assert.strictEqual(res.labelMatch![0].end, 3); }); test('scoreItem - proper target offset #3', function () { @@ -338,19 +338,19 @@ suite('Fuzzy Scorer', () => { const res = scoreItem(resource, 'debug', true, ResourceAccessor); - assert.equal(res.descriptionMatch!.length, 3); - assert.equal(res.descriptionMatch![0].start, 9); - assert.equal(res.descriptionMatch![0].end, 10); - assert.equal(res.descriptionMatch![1].start, 36); - assert.equal(res.descriptionMatch![1].end, 37); - assert.equal(res.descriptionMatch![2].start, 40); - assert.equal(res.descriptionMatch![2].end, 41); + assert.strictEqual(res.descriptionMatch!.length, 3); + assert.strictEqual(res.descriptionMatch![0].start, 9); + assert.strictEqual(res.descriptionMatch![0].end, 10); + assert.strictEqual(res.descriptionMatch![1].start, 36); + assert.strictEqual(res.descriptionMatch![1].end, 37); + assert.strictEqual(res.descriptionMatch![2].start, 40); + assert.strictEqual(res.descriptionMatch![2].end, 41); - assert.equal(res.labelMatch!.length, 2); - assert.equal(res.labelMatch![0].start, 9); - assert.equal(res.labelMatch![0].end, 10); - assert.equal(res.labelMatch![1].start, 20); - assert.equal(res.labelMatch![1].end, 21); + assert.strictEqual(res.labelMatch!.length, 2); + assert.strictEqual(res.labelMatch![0].start, 9); + assert.strictEqual(res.labelMatch![0].end, 10); + assert.strictEqual(res.labelMatch![1].start, 20); + assert.strictEqual(res.labelMatch![1].end, 21); }); test('scoreItem - no match unless query contained in sequence', function () { @@ -394,27 +394,27 @@ suite('Fuzzy Scorer', () => { let query = ResourceAccessor.getItemPath(resourceA); let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Full resource B path query = ResourceAccessor.getItemPath(resourceB); res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename prefix', function () { @@ -426,27 +426,27 @@ suite('Fuzzy Scorer', () => { let query = ResourceAccessor.getItemLabel(resourceA); let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Full resource B basename query = ResourceAccessor.getItemLabel(resourceB); res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename camelcase', function () { @@ -458,27 +458,27 @@ suite('Fuzzy Scorer', () => { let query = 'fA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // resource B camelcase query = 'fB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - basename scores', function () { @@ -490,27 +490,27 @@ suite('Fuzzy Scorer', () => { let query = 'fileA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Resource B part of basename query = 'fileB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - path scores', function () { @@ -522,27 +522,27 @@ suite('Fuzzy Scorer', () => { let query = 'pathfileA'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); // Resource B part of path query = 'pathfileB'; res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter basenames', function () { @@ -554,14 +554,14 @@ suite('Fuzzy Scorer', () => { let query = 'somepath'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter basenames (match on basename)', function () { @@ -573,14 +573,14 @@ suite('Fuzzy Scorer', () => { let query = 'file'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); }); test('compareFilesByScore - prefer shorter paths', function () { @@ -592,14 +592,14 @@ suite('Fuzzy Scorer', () => { let query = 'somepath'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer shorter paths (bug #17443)', function () { @@ -610,9 +610,9 @@ suite('Fuzzy Scorer', () => { let query = 'co/te'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); - assert.equal(res[2], resourceC); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); + assert.strictEqual(res[2], resourceC); }); test('compareFilesByScore - prefer matches in label over description if scores are otherwise equal', function () { @@ -622,8 +622,8 @@ suite('Fuzzy Scorer', () => { let query = 'partsquick'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer camel case matches', function () { @@ -632,12 +632,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['npe', 'NPE']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -648,12 +648,12 @@ suite('Fuzzy Scorer', () => { let query = 'AH'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (label)', function () { @@ -663,12 +663,12 @@ suite('Fuzzy Scorer', () => { let query = 'xp'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (path)', function () { @@ -678,12 +678,12 @@ suite('Fuzzy Scorer', () => { let query = 'xp'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - prefer more compact matches (label and path)', function () { @@ -693,12 +693,12 @@ suite('Fuzzy Scorer', () => { let query = 'exfile'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); }); test('compareFilesByScore - avoid match scattering (bug #34210)', function () { @@ -710,18 +710,18 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'modu1\\index.js' : 'modu1/index.js'; let res = [resourceA, resourceB, resourceC, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); query = isWindows ? 'un1\\index.js' : 'un1/index.js'; res = [resourceA, resourceB, resourceC, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceC, resourceB, resourceA, resourceD].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #21019 1.)', function () { @@ -732,10 +732,10 @@ suite('Fuzzy Scorer', () => { let query = 'StatVideoindex'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); }); test('compareFilesByScore - avoid match scattering (bug #21019 2.)', function () { @@ -745,10 +745,10 @@ suite('Fuzzy Scorer', () => { let query = 'reproreduxts'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #26649)', function () { @@ -759,10 +759,10 @@ suite('Fuzzy Scorer', () => { let query = 'bookpageIndex'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceC); + assert.strictEqual(res[0], resourceC); }); test('compareFilesByScore - avoid match scattering (bug #33247)', function () { @@ -772,10 +772,10 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'ui\\icons' : 'ui/icons'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #33247 comment)', function () { @@ -785,10 +785,10 @@ suite('Fuzzy Scorer', () => { let query = isWindows ? 'ui\\input\\index' : 'ui/input/index'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #36166)', function () { @@ -798,10 +798,10 @@ suite('Fuzzy Scorer', () => { let query = 'djancosig'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #32918)', function () { @@ -812,14 +812,14 @@ suite('Fuzzy Scorer', () => { let query = 'protectedconfig.php'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); res = [resourceC, resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceC); - assert.equal(res[2], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceC); + assert.strictEqual(res[2], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14879)', function () { @@ -829,10 +829,10 @@ suite('Fuzzy Scorer', () => { let query = 'gradientmain'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14727 1)', function () { @@ -842,10 +842,10 @@ suite('Fuzzy Scorer', () => { let query = 'abc'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #14727 2)', function () { @@ -855,10 +855,10 @@ suite('Fuzzy Scorer', () => { let query = 'xyz'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #18381)', function () { @@ -868,10 +868,10 @@ suite('Fuzzy Scorer', () => { let query = 'async'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #35572)', function () { @@ -881,10 +881,10 @@ suite('Fuzzy Scorer', () => { let query = 'partisettings'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #36810)', function () { @@ -894,10 +894,10 @@ suite('Fuzzy Scorer', () => { let query = 'tipsindex.cshtml'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer shorter hit (bug #20546)', function () { @@ -907,10 +907,10 @@ suite('Fuzzy Scorer', () => { let query = 'listview'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - avoid match scattering (bug #12095)', function () { @@ -921,10 +921,10 @@ suite('Fuzzy Scorer', () => { let query = 'filesexplorerview.ts'; let res = [resourceA, resourceB, resourceC].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceA, resourceC, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer case match (bug #96122)', function () { @@ -934,10 +934,10 @@ suite('Fuzzy Scorer', () => { let query = 'Lists.php'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); + assert.strictEqual(res[0], resourceB); }); test('compareFilesByScore - prefer shorter match (bug #103052) - foo bar', function () { @@ -946,12 +946,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['foo bar', 'foobar']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -961,12 +961,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['payment model', 'paymentmodel']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -976,12 +976,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['color js', 'colorjs']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); } }); @@ -992,22 +992,22 @@ suite('Fuzzy Scorer', () => { let query = 'Color'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); query = 'color'; res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); }); test('compareFilesByScore - prefer prefix (bug #103052)', function () { @@ -1017,12 +1017,12 @@ suite('Fuzzy Scorer', () => { let query = 'smoke main.ts'; let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceA); - assert.equal(res[1], resourceB); + assert.strictEqual(res[0], resourceA); + assert.strictEqual(res[1], resourceB); }); test('compareFilesByScore - boost better prefix match if multiple queries are used', function () { @@ -1031,12 +1031,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['workbench.ts browser', 'browser workbench.ts', 'browser workbench', 'workbench browser']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); @@ -1046,12 +1046,12 @@ suite('Fuzzy Scorer', () => { for (const query of ['window browser', 'window.ts browser']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); @@ -1061,73 +1061,73 @@ suite('Fuzzy Scorer', () => { for (const query of ['m life, life m']) { let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); - assert.equal(res[0], resourceB); - assert.equal(res[1], resourceA); + assert.strictEqual(res[0], resourceB); + assert.strictEqual(res[1], resourceA); } }); test('prepareQuery', () => { - assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); - assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); - assert.equal(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); - assert.equal(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); - assert.equal(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); - assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); - assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); + assert.strictEqual(prepareQuery(' f*a ').normalized, 'fa'); + assert.strictEqual(prepareQuery('model Tester.ts').original, 'model Tester.ts'); + assert.strictEqual(prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); + assert.strictEqual(prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); + assert.strictEqual(prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); + assert.strictEqual(prepareQuery('ModelTester.ts').containsPathSeparator, false); + assert.strictEqual(prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); // with spaces - let query = scorer.prepareQuery('He*llo World'); - assert.equal(query.original, 'He*llo World'); - assert.equal(query.normalized, 'HelloWorld'); - assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); - assert.equal(query.values?.length, 2); - assert.equal(query.values?.[0].original, 'He*llo'); - assert.equal(query.values?.[0].normalized, 'Hello'); - assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].normalized, 'World'); - assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + let query = prepareQuery('He*llo World'); + assert.strictEqual(query.original, 'He*llo World'); + assert.strictEqual(query.normalized, 'HelloWorld'); + assert.strictEqual(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); + assert.strictEqual(query.values?.length, 2); + assert.strictEqual(query.values?.[0].original, 'He*llo'); + assert.strictEqual(query.values?.[0].normalized, 'Hello'); + assert.strictEqual(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[1].original, 'World'); + assert.strictEqual(query.values?.[1].normalized, 'World'); + assert.strictEqual(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); - let restoredQuery = scorer.pieceToQuery(query.values!); - assert.equal(restoredQuery.original, query.original); - assert.equal(restoredQuery.values?.length, query.values?.length); - assert.equal(restoredQuery.containsPathSeparator, query.containsPathSeparator); + let restoredQuery = pieceToQuery(query.values!); + assert.strictEqual(restoredQuery.original, query.original); + assert.strictEqual(restoredQuery.values?.length, query.values?.length); + assert.strictEqual(restoredQuery.containsPathSeparator, query.containsPathSeparator); // with spaces that are empty - query = scorer.prepareQuery(' Hello World '); - assert.equal(query.original, ' Hello World '); - assert.equal(query.originalLowercase, ' Hello World '.toLowerCase()); - assert.equal(query.normalized, 'HelloWorld'); - assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); - assert.equal(query.values?.length, 2); - assert.equal(query.values?.[0].original, 'Hello'); - assert.equal(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[0].normalized, 'Hello'); - assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); - assert.equal(query.values?.[1].original, 'World'); - assert.equal(query.values?.[1].originalLowercase, 'World'.toLowerCase()); - assert.equal(query.values?.[1].normalized, 'World'); - assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); + query = prepareQuery(' Hello World '); + assert.strictEqual(query.original, ' Hello World '); + assert.strictEqual(query.originalLowercase, ' Hello World '.toLowerCase()); + assert.strictEqual(query.normalized, 'HelloWorld'); + assert.strictEqual(query.normalizedLowercase, 'HelloWorld'.toLowerCase()); + assert.strictEqual(query.values?.length, 2); + assert.strictEqual(query.values?.[0].original, 'Hello'); + assert.strictEqual(query.values?.[0].originalLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[0].normalized, 'Hello'); + assert.strictEqual(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase()); + assert.strictEqual(query.values?.[1].original, 'World'); + assert.strictEqual(query.values?.[1].originalLowercase, 'World'.toLowerCase()); + assert.strictEqual(query.values?.[1].normalized, 'World'); + assert.strictEqual(query.values?.[1].normalizedLowercase, 'World'.toLowerCase()); // Path related if (isWindows) { - assert.equal(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true); - assert.equal(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); - assert.equal(scorer.prepareQuery('C:/some/path').containsPathSeparator, true); + assert.strictEqual(prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path'); + assert.strictEqual(prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path'); + assert.strictEqual(prepareQuery('C:\\some\\path').containsPathSeparator, true); + assert.strictEqual(prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path'); + assert.strictEqual(prepareQuery('C:/some/path').normalized, 'C:\\some\\path'); + assert.strictEqual(prepareQuery('C:/some/path').containsPathSeparator, true); } else { - assert.equal(scorer.prepareQuery('/some/path').pathNormalized, '/some/path'); - assert.equal(scorer.prepareQuery('/some/path').normalized, '/some/path'); - assert.equal(scorer.prepareQuery('/some/path').containsPathSeparator, true); - assert.equal(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path'); - assert.equal(scorer.prepareQuery('\\some\\path').normalized, '/some/path'); - assert.equal(scorer.prepareQuery('\\some\\path').containsPathSeparator, true); + assert.strictEqual(prepareQuery('/some/path').pathNormalized, '/some/path'); + assert.strictEqual(prepareQuery('/some/path').normalized, '/some/path'); + assert.strictEqual(prepareQuery('/some/path').containsPathSeparator, true); + assert.strictEqual(prepareQuery('\\some\\path').pathNormalized, '/some/path'); + assert.strictEqual(prepareQuery('\\some\\path').normalized, '/some/path'); + assert.strictEqual(prepareQuery('\\some\\path').containsPathSeparator, true); } }); @@ -1138,18 +1138,18 @@ suite('Fuzzy Scorer', () => { let [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HeLlo-World', offset); assert.ok(score); - assert.equal(matches.length, 1); - assert.equal(matches[0].start, 0 + offset); - assert.equal(matches[0].end, target.length + offset); + assert.strictEqual(matches.length, 1); + assert.strictEqual(matches[0].start, 0 + offset); + assert.strictEqual(matches[0].end, target.length + offset); [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HW', offset); assert.ok(score); - assert.equal(matches.length, 2); - assert.equal(matches[0].start, 0 + offset); - assert.equal(matches[0].end, 1 + offset); - assert.equal(matches[1].start, 6 + offset); - assert.equal(matches[1].end, 7 + offset); + assert.strictEqual(matches.length, 2); + assert.strictEqual(matches[0].start, 0 + offset); + assert.strictEqual(matches[0].end, 1 + offset); + assert.strictEqual(matches[1].start, 6 + offset); + assert.strictEqual(matches[1].end, 7 + offset); } }); @@ -1169,8 +1169,8 @@ suite('Fuzzy Scorer', () => { const firstAndSecondSingleMatch = firstAndSecondSingleMatches[i]; if (multiMatch && firstAndSecondSingleMatch) { - assert.equal(multiMatch.start, firstAndSecondSingleMatch.start); - assert.equal(multiMatch.end, firstAndSecondSingleMatch.end); + assert.strictEqual(multiMatch.start, firstAndSecondSingleMatch.start); + assert.strictEqual(multiMatch.end, firstAndSecondSingleMatch.end); } else { assert.fail(); } @@ -1178,8 +1178,8 @@ suite('Fuzzy Scorer', () => { } function assertNoScore() { - assert.equal(multiScore, undefined); - assert.equal(multiMatches.length, 0); + assert.strictEqual(multiScore, undefined); + assert.strictEqual(multiMatches.length, 0); } assertScore(); diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/common/glob.test.ts similarity index 98% rename from src/vs/base/test/node/glob.test.ts rename to src/vs/base/test/common/glob.test.ts index 72c8f23ff8..a37a9efa9d 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -2,9 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; import * as glob from 'vs/base/common/glob'; +import { sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; suite('Glob', () => { @@ -63,10 +64,12 @@ suite('Glob', () => { function assertGlobMatch(pattern: string | glob.IRelativePattern, input: string) { assert(glob.match(pattern, input), `${pattern} should match ${input}`); + assert(glob.match(pattern, nativeSep(input)), `${pattern} should match ${nativeSep(input)}`); } function assertNoGlobMatch(pattern: string | glob.IRelativePattern, input: string) { assert(!glob.match(pattern, input), `${pattern} should not match ${input}`); + assert(!glob.match(pattern, nativeSep(input)), `${pattern} should not match ${nativeSep(input)}`); } test('simple', () => { @@ -538,9 +541,13 @@ suite('Glob', () => { }); test('full path', function () { - let p = 'testing/this/foo.txt'; + assertGlobMatch('testing/this/foo.txt', 'testing/this/foo.txt'); + // assertGlobMatch('testing/this/foo.txt', 'testing\\this\\foo.txt'); + }); - assert(glob.match(p, nativeSep('testing/this/foo.txt'))); + test('ending path', function () { + assertGlobMatch('**/testing/this/foo.txt', 'some/path/testing/this/foo.txt'); + // assertGlobMatch('**/testing/this/foo.txt', 'some\\path\\testing\\this\\foo.txt'); }); test('prefix agnostic', function () { @@ -946,7 +953,7 @@ suite('Glob', () => { } function nativeSep(slashPath: string): string { - return slashPath.replace(/\//g, path.sep); + return slashPath.replace(/\//g, sep); } test('relative pattern - glob star', function () { diff --git a/src/vs/base/test/common/iconLabels.test.ts b/src/vs/base/test/common/iconLabels.test.ts new file mode 100644 index 0000000000..f492d327a4 --- /dev/null +++ b/src/vs/base/test/common/iconLabels.test.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IMatch } from 'vs/base/common/filters'; +import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons, escapeIcons, markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels'; + +export interface IIconFilter { + // Returns null if word doesn't match. + (query: string, target: IParsedLabelWithIcons): IMatch[] | null; +} + +function filterOk(filter: IIconFilter, word: string, target: IParsedLabelWithIcons, highlights?: { start: number; end: number; }[]) { + let r = filter(word, target); + assert(r); + if (highlights) { + assert.deepEqual(r, highlights); + } +} + +suite('Icon Labels', () => { + test('matchesFuzzyIconAware', () => { + + // Camel Case + + filterOk(matchesFuzzyIconAware, 'ccr', parseLabelWithIcons('$(codicon)CamelCaseRocks$(codicon)'), [ + { start: 10, end: 11 }, + { start: 15, end: 16 }, + { start: 19, end: 20 } + ]); + + filterOk(matchesFuzzyIconAware, 'ccr', parseLabelWithIcons('$(codicon) CamelCaseRocks $(codicon)'), [ + { start: 11, end: 12 }, + { start: 16, end: 17 }, + { start: 20, end: 21 } + ]); + + filterOk(matchesFuzzyIconAware, 'iut', parseLabelWithIcons('$(codicon) Indent $(octico) Using $(octic) Tpaces'), [ + { start: 11, end: 12 }, + { start: 28, end: 29 }, + { start: 43, end: 44 }, + ]); + + // Prefix + + filterOk(matchesFuzzyIconAware, 'using', parseLabelWithIcons('$(codicon) Indent Using Spaces'), [ + { start: 18, end: 23 }, + ]); + + // Broken Codicon + + filterOk(matchesFuzzyIconAware, 'codicon', parseLabelWithIcons('This $(codicon Indent Using Spaces'), [ + { start: 7, end: 14 }, + ]); + + filterOk(matchesFuzzyIconAware, 'indent', parseLabelWithIcons('This $codicon Indent Using Spaces'), [ + { start: 14, end: 20 }, + ]); + + // Testing #59343 + filterOk(matchesFuzzyIconAware, 'unt', parseLabelWithIcons('$(primitive-dot) $(file-text) Untitled-1'), [ + { start: 30, end: 33 }, + ]); + }); + + test('stripIcons', () => { + assert.strictEqual(stripIcons('Hello World'), 'Hello World'); + assert.strictEqual(stripIcons('$(Hello World'), '$(Hello World'); + assert.strictEqual(stripIcons('$(Hello) World'), ' World'); + assert.strictEqual(stripIcons('$(Hello) W$(oi)rld'), ' Wrld'); + }); + + + test('escapeIcons', () => { + assert.strictEqual(escapeIcons('Hello World'), 'Hello World'); + assert.strictEqual(escapeIcons('$(Hello World'), '$(Hello World'); + assert.strictEqual(escapeIcons('$(Hello) World'), '\\$(Hello) World'); + assert.strictEqual(escapeIcons('\\$(Hello) W$(oi)rld'), '\\$(Hello) W\\$(oi)rld'); + }); + + test('markdownEscapeEscapedIcons', () => { + assert.strictEqual(markdownEscapeEscapedIcons('Hello World'), 'Hello World'); + assert.strictEqual(markdownEscapeEscapedIcons('$(Hello) World'), '$(Hello) World'); + assert.strictEqual(markdownEscapeEscapedIcons('\\$(Hello) World'), '\\\\$(Hello) World'); + }); +}); diff --git a/src/vs/base/test/common/keyCodes.test.ts b/src/vs/base/test/common/keyCodes.test.ts index d141a5dbea..4a538c4c06 100644 --- a/src/vs/base/test/common/keyCodes.test.ts +++ b/src/vs/base/test/common/keyCodes.test.ts @@ -10,7 +10,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; suite('keyCodes', () => { function testBinaryEncoding(expected: Keybinding | null, k: number, OS: OperatingSystem): void { - assert.deepEqual(createKeybinding(k, OS), expected); + assert.deepStrictEqual(createKeybinding(k, OS), expected); } test('MAC binary encoding', () => { diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index d30c1fab6e..46298771fa 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -5,109 +5,101 @@ import * as assert from 'assert'; import * as labels from 'vs/base/common/labels'; -import * as platform from 'vs/base/common/platform'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; suite('Labels', () => { - test('shorten - windows', () => { - if (!platform.isWindows) { - assert.ok(true); - return; - } + (!isWindows ? test.skip : test)('shorten - windows', () => { // nothing to shorten - assert.deepEqual(labels.shorten(['a']), ['a']); - assert.deepEqual(labels.shorten(['a', 'b']), ['a', 'b']); - assert.deepEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['a']), ['a']); + assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths - assert.deepEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); // same beginning - assert.deepEqual(labels.shorten(['a', 'a\\b']), ['a', '…\\b']); - assert.deepEqual(labels.shorten(['a\\b', 'a\\b\\c']), ['…\\b', '…\\c']); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c']), ['a', '…\\b', '…\\c']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'x:\\a\\c']), ['x:\\…\\b', 'x:\\…\\c']); - assert.deepEqual(labels.shorten(['\\\\a\\b', '\\\\a\\c']), ['\\\\a\\b', '\\\\a\\c']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b']), ['a', '…\\b']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'a\\b\\c']), ['…\\b', '…\\c']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c']), ['a', '…\\b', '…\\c']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'x:\\a\\c']), ['x:\\…\\b', 'x:\\…\\c']); + assert.deepStrictEqual(labels.shorten(['\\\\a\\b', '\\\\a\\c']), ['\\\\a\\b', '\\\\a\\c']); // same ending - assert.deepEqual(labels.shorten(['a', 'b\\a']), ['a', 'b\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\c']), ['a\\…', 'd\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c\\d', 'f\\b\\c\\d']), ['a\\…', 'f\\…']); - assert.deepEqual(labels.shorten(['d\\e\\a\\b\\c', 'd\\b\\c']), ['…\\a\\…', 'd\\b\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c\\d', 'a\\f\\b\\c\\d']), ['a\\b\\…', '…\\f\\…']); - assert.deepEqual(labels.shorten(['a\\b\\a', 'b\\b\\a']), ['a\\b\\…', 'b\\b\\…']); - assert.deepEqual(labels.shorten(['d\\f\\a\\b\\c', 'h\\d\\b\\c']), ['…\\a\\…', 'h\\…']); - assert.deepEqual(labels.shorten(['a\\b\\c', 'x:\\0\\a\\b\\c']), ['a\\b\\c', 'x:\\0\\…']); - assert.deepEqual(labels.shorten(['x:\\a\\b\\c', 'x:\\0\\a\\b\\c']), ['x:\\a\\…', 'x:\\0\\…']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'y:\\a\\b']), ['x:\\…', 'y:\\…']); - assert.deepEqual(labels.shorten(['x:\\a', 'x:\\c']), ['x:\\a', 'x:\\c']); - assert.deepEqual(labels.shorten(['x:\\a\\b', 'y:\\x\\a\\b']), ['x:\\…', 'y:\\…']); - assert.deepEqual(labels.shorten(['\\\\x\\b', '\\\\y\\b']), ['\\\\x\\…', '\\\\y\\…']); - assert.deepEqual(labels.shorten(['\\\\x\\a', '\\\\x\\b']), ['\\\\x\\a', '\\\\x\\b']); + assert.deepStrictEqual(labels.shorten(['a', 'b\\a']), ['a', 'b\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\c']), ['a\\…', 'd\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c\\d', 'f\\b\\c\\d']), ['a\\…', 'f\\…']); + assert.deepStrictEqual(labels.shorten(['d\\e\\a\\b\\c', 'd\\b\\c']), ['…\\a\\…', 'd\\b\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c\\d', 'a\\f\\b\\c\\d']), ['a\\b\\…', '…\\f\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\a', 'b\\b\\a']), ['a\\b\\…', 'b\\b\\…']); + assert.deepStrictEqual(labels.shorten(['d\\f\\a\\b\\c', 'h\\d\\b\\c']), ['…\\a\\…', 'h\\…']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'x:\\0\\a\\b\\c']), ['a\\b\\c', 'x:\\0\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b\\c', 'x:\\0\\a\\b\\c']), ['x:\\a\\…', 'x:\\0\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'y:\\a\\b']), ['x:\\…', 'y:\\…']); + assert.deepStrictEqual(labels.shorten(['x:\\a', 'x:\\c']), ['x:\\a', 'x:\\c']); + assert.deepStrictEqual(labels.shorten(['x:\\a\\b', 'y:\\x\\a\\b']), ['x:\\…', 'y:\\…']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\b', '\\\\y\\b']), ['\\\\x\\…', '\\\\y\\…']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\a', '\\\\x\\b']), ['\\\\x\\a', '\\\\x\\b']); // same name ending - assert.deepEqual(labels.shorten(['a\\b', 'a\\c', 'a\\e-b']), ['…\\b', '…\\c', '…\\e-b']); + assert.deepStrictEqual(labels.shorten(['a\\b', 'a\\c', 'a\\e-b']), ['…\\b', '…\\c', '…\\e-b']); // same in the middle - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\e']), ['…\\c', '…\\e']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\e']), ['…\\c', '…\\e']); // case-sensetive - assert.deepEqual(labels.shorten(['a\\b\\c', 'd\\b\\C']), ['…\\c', '…\\C']); + assert.deepStrictEqual(labels.shorten(['a\\b\\c', 'd\\b\\C']), ['…\\c', '…\\C']); // empty or null - assert.deepEqual(labels.shorten(['', null!]), ['.\\', null]); + assert.deepStrictEqual(labels.shorten(['', null!]), ['.\\', null]); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']), ['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']); - assert.deepEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']); - assert.deepEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']); - assert.deepEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']), ['a', 'a\\b', 'a\\b\\c', 'd\\b\\c', 'd\\b']); + assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']); + assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']); + assert.deepStrictEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']); }); - test('shorten - not windows', () => { - if (platform.isWindows) { - assert.ok(true); - return; - } + (isWindows ? test.skip : test)('shorten - not windows', () => { // nothing to shorten - assert.deepEqual(labels.shorten(['a']), ['a']); - assert.deepEqual(labels.shorten(['a', 'b']), ['a', 'b']); - assert.deepEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['a']), ['a']); + assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths - assert.deepEqual(labels.shorten(['a/b', 'c/d', 'e/f']), ['…/b', '…/d', '…/f']); + assert.deepStrictEqual(labels.shorten(['a/b', 'c/d', 'e/f']), ['…/b', '…/d', '…/f']); // same beginning - assert.deepEqual(labels.shorten(['a', 'a/b']), ['a', '…/b']); - assert.deepEqual(labels.shorten(['a/b', 'a/b/c']), ['…/b', '…/c']); - assert.deepEqual(labels.shorten(['a', 'a/b', 'a/b/c']), ['a', '…/b', '…/c']); - assert.deepEqual(labels.shorten(['/a/b', '/a/c']), ['/a/b', '/a/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b']), ['a', '…/b']); + assert.deepStrictEqual(labels.shorten(['a/b', 'a/b/c']), ['…/b', '…/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c']), ['a', '…/b', '…/c']); + assert.deepStrictEqual(labels.shorten(['/a/b', '/a/c']), ['/a/b', '/a/c']); // same ending - assert.deepEqual(labels.shorten(['a', 'b/a']), ['a', 'b/…']); - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/c']), ['a/…', 'd/…']); - assert.deepEqual(labels.shorten(['a/b/c/d', 'f/b/c/d']), ['a/…', 'f/…']); - assert.deepEqual(labels.shorten(['d/e/a/b/c', 'd/b/c']), ['…/a/…', 'd/b/…']); - assert.deepEqual(labels.shorten(['a/b/c/d', 'a/f/b/c/d']), ['a/b/…', '…/f/…']); - assert.deepEqual(labels.shorten(['a/b/a', 'b/b/a']), ['a/b/…', 'b/b/…']); - assert.deepEqual(labels.shorten(['d/f/a/b/c', 'h/d/b/c']), ['…/a/…', 'h/…']); - assert.deepEqual(labels.shorten(['/x/b', '/y/b']), ['/x/…', '/y/…']); + assert.deepStrictEqual(labels.shorten(['a', 'b/a']), ['a', 'b/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/c']), ['a/…', 'd/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c/d', 'f/b/c/d']), ['a/…', 'f/…']); + assert.deepStrictEqual(labels.shorten(['d/e/a/b/c', 'd/b/c']), ['…/a/…', 'd/b/…']); + assert.deepStrictEqual(labels.shorten(['a/b/c/d', 'a/f/b/c/d']), ['a/b/…', '…/f/…']); + assert.deepStrictEqual(labels.shorten(['a/b/a', 'b/b/a']), ['a/b/…', 'b/b/…']); + assert.deepStrictEqual(labels.shorten(['d/f/a/b/c', 'h/d/b/c']), ['…/a/…', 'h/…']); + assert.deepStrictEqual(labels.shorten(['/x/b', '/y/b']), ['/x/…', '/y/…']); // same name ending - assert.deepEqual(labels.shorten(['a/b', 'a/c', 'a/e-b']), ['…/b', '…/c', '…/e-b']); + assert.deepStrictEqual(labels.shorten(['a/b', 'a/c', 'a/e-b']), ['…/b', '…/c', '…/e-b']); // same in the middle - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/e']), ['…/c', '…/e']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/e']), ['…/c', '…/e']); // case-sensitive - assert.deepEqual(labels.shorten(['a/b/c', 'd/b/C']), ['…/c', '…/C']); + assert.deepStrictEqual(labels.shorten(['a/b/c', 'd/b/C']), ['…/c', '…/C']); // empty or null - assert.deepEqual(labels.shorten(['', null!]), ['./', null]); + assert.deepStrictEqual(labels.shorten(['', null!]), ['./', null]); - assert.deepEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']); - assert.deepEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']); - assert.deepEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']); + assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']); + assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']); }); test('template', () => { @@ -142,41 +134,32 @@ suite('Labels', () => { assert.strictEqual(labels.template(t, { dirty: '* ', activeEditorShort: 'somefile.txt', rootName: 'monaco', appName: 'Visual Studio Code', separator: { label: ' - ' } }), '* somefile.txt - monaco - Visual Studio Code'); }); - test('getBaseLabel - unix', () => { - if (platform.isWindows) { - assert.ok(true); - return; - } - - assert.equal(labels.getBaseLabel('/some/folder/file.txt'), 'file.txt'); - assert.equal(labels.getBaseLabel('/some/folder'), 'folder'); - assert.equal(labels.getBaseLabel('/'), '/'); + (isWindows ? test.skip : test)('getBaseLabel - unix', () => { + assert.strictEqual(labels.getBaseLabel('/some/folder/file.txt'), 'file.txt'); + assert.strictEqual(labels.getBaseLabel('/some/folder'), 'folder'); + assert.strictEqual(labels.getBaseLabel('/'), '/'); }); - test('getBaseLabel - windows', () => { - if (!platform.isWindows) { - assert.ok(true); - return; - } - - assert.equal(labels.getBaseLabel('c:'), 'C:'); - assert.equal(labels.getBaseLabel('c:\\'), 'C:'); - assert.equal(labels.getBaseLabel('c:\\some\\folder\\file.txt'), 'file.txt'); - assert.equal(labels.getBaseLabel('c:\\some\\folder'), 'folder'); + (!isWindows ? test.skip : test)('getBaseLabel - windows', () => { + assert.strictEqual(labels.getBaseLabel('c:'), 'C:'); + assert.strictEqual(labels.getBaseLabel('c:\\'), 'C:'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\folder\\file.txt'), 'file.txt'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\folder'), 'folder'); + assert.strictEqual(labels.getBaseLabel('c:\\some\\f:older'), 'f:older'); // https://github.com/microsoft/vscode-remote-release/issues/4227 }); test('mnemonicButtonLabel', () => { - assert.equal(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); - assert.equal(labels.mnemonicButtonLabel(''), ''); - if (platform.isWindows) { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); - } else if (platform.isMacintosh) { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello World'), 'Hello World'); + assert.strictEqual(labels.mnemonicButtonLabel(''), ''); + if (isWindows) { + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello && World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do ¬ Save && Continue'); + } else if (isMacintosh) { + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do not Save & Continue'); } else { - assert.equal(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); - assert.equal(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); + assert.strictEqual(labels.mnemonicButtonLabel('Hello & World'), 'Hello & World'); + assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); } }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index c905c9d7ac..fcb27ab896 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { DisposableStore, dispose, IDisposable, MultiDisposeError, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; @@ -95,8 +96,8 @@ suite('Lifecycle', () => { let array = [{ dispose() { } }, { dispose() { } }]; let array2 = dispose(array); - assert.equal(array.length, 2); - assert.equal(array2.length, 0); + assert.strictEqual(array.length, 2); + assert.strictEqual(array2.length, 0); assert.ok(array !== array2); let set = new Set([{ dispose() { } }, { dispose() { } }]); @@ -165,27 +166,27 @@ suite('Reference Collection', () => { const ref1 = collection.acquire('test'); assert(ref1); - assert.equal(ref1.object, 4); - assert.equal(collection.count, 1); + assert.strictEqual(ref1.object, 4); + assert.strictEqual(collection.count, 1); ref1.dispose(); - assert.equal(collection.count, 0); + assert.strictEqual(collection.count, 0); const ref2 = collection.acquire('test'); const ref3 = collection.acquire('test'); - assert.equal(ref2.object, ref3.object); - assert.equal(collection.count, 1); + assert.strictEqual(ref2.object, ref3.object); + assert.strictEqual(collection.count, 1); const ref4 = collection.acquire('monkey'); - assert.equal(ref4.object, 6); - assert.equal(collection.count, 2); + assert.strictEqual(ref4.object, 6); + assert.strictEqual(collection.count, 2); ref2.dispose(); - assert.equal(collection.count, 2); + assert.strictEqual(collection.count, 2); ref3.dispose(); - assert.equal(collection.count, 1); + assert.strictEqual(collection.count, 1); ref4.dispose(); - assert.equal(collection.count, 0); + assert.strictEqual(collection.count, 0); }); }); diff --git a/src/vs/base/test/common/linkedList.test.ts b/src/vs/base/test/common/linkedList.test.ts index 00413afba9..3b9c2d62f5 100644 --- a/src/vs/base/test/common/linkedList.test.ts +++ b/src/vs/base/test/common/linkedList.test.ts @@ -11,19 +11,19 @@ suite('LinkedList', function () { function assertElements(list: LinkedList, ...elements: E[]) { // check size - assert.equal(list.size, elements.length); + assert.strictEqual(list.size, elements.length); // assert toArray - assert.deepEqual(Array.from(list), elements); + assert.deepStrictEqual(Array.from(list), elements); // assert Symbol.iterator (1) - assert.deepEqual([...list], elements); + assert.deepStrictEqual([...list], elements); // assert Symbol.iterator (2) for (const item of list) { - assert.equal(item, elements.shift()); + assert.strictEqual(item, elements.shift()); } - assert.equal(elements.length, 0); + assert.strictEqual(elements.length, 0); } test('Push/Iter', () => { @@ -123,15 +123,14 @@ suite('LinkedList', function () { assertElements(list, 'a', 'b'); let a = list.shift(); - assert.equal(a, 'a'); + assert.strictEqual(a, 'a'); assertElements(list, 'b'); list.unshift('a'); assertElements(list, 'a', 'b'); let b = list.pop(); - assert.equal(b, 'b'); + assert.strictEqual(b, 'b'); assertElements(list, 'a'); - }); }); diff --git a/src/vs/base/test/common/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts index e63703d4c0..60e3a8c958 100644 --- a/src/vs/base/test/common/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -8,61 +8,61 @@ import { parseLinkedText } from 'vs/base/common/linkedText'; suite('LinkedText', () => { test('parses correctly', () => { - assert.deepEqual(parseLinkedText('').nodes, []); - assert.deepEqual(parseLinkedText('hello').nodes, ['hello']); - assert.deepEqual(parseLinkedText('hello there').nodes, ['hello there']); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href).').nodes, [ + assert.deepStrictEqual(parseLinkedText('').nodes, []); + assert.deepStrictEqual(parseLinkedText('hello').nodes, ['hello']); + assert.deepStrictEqual(parseLinkedText('hello there').nodes, ['hello there']); + assert.deepStrictEqual(parseLinkedText('Some message with [link text](http://link.href).').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [link text](http://link.href "and a title").').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href', title: 'and a title' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href \'and a title\').').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [link text](http://link.href \'and a title\').').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href', title: 'and a title' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a \'title\'").').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [link text](http://link.href "and a \'title\'").').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href', title: 'and a \'title\'' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href \'and a "title"\').').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [link text](http://link.href \'and a "title"\').').nodes, [ 'Some message with ', { label: 'link text', href: 'http://link.href', title: 'and a "title"' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [link text](random stuff).').nodes, [ 'Some message with [link text](random stuff).' ]); - assert.deepEqual(parseLinkedText('Some message with [https link](https://link.href).').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [https link](https://link.href).').nodes, [ 'Some message with ', { label: 'https link', href: 'https://link.href' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [https link](https:).').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [https link](https:).').nodes, [ 'Some message with [https link](https:).' ]); - assert.deepEqual(parseLinkedText('Some message with [a command](command:foobar).').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [a command](command:foobar).').nodes, [ 'Some message with ', { label: 'a command', href: 'command:foobar' }, '.' ]); - assert.deepEqual(parseLinkedText('Some message with [a command](command:).').nodes, [ + assert.deepStrictEqual(parseLinkedText('Some message with [a command](command:).').nodes, [ 'Some message with [a command](command:).' ]); - assert.deepEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...').nodes, [ + assert.deepStrictEqual(parseLinkedText('link [one](command:foo "nice") and link [two](http://foo)...').nodes, [ 'link ', { label: 'one', href: 'command:foo', title: 'nice' }, ' and link ', { label: 'two', href: 'http://foo' }, '...' ]); - assert.deepEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...').nodes, [ + assert.deepStrictEqual(parseLinkedText('link\n[one](command:foo "nice")\nand link [two](http://foo)...').nodes, [ 'link\n', { label: 'one', href: 'command:foo', title: 'nice' }, '\nand link ', diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 5118454968..88c9b1b6ba 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache, UriIterator, ConfigKeysIterator } from 'vs/base/common/map'; import * as assert from 'assert'; +import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache, UriIterator, ConfigKeysIterator } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { extUriIgnorePathCase } from 'vs/base/common/resources'; @@ -16,8 +16,8 @@ suite('Map', () => { map.set('bk', 'bv'); assert.deepStrictEqual([...map.keys()], ['ak', 'bk']); assert.deepStrictEqual([...map.values()], ['av', 'bv']); - assert.equal(map.first, 'av'); - assert.equal(map.last, 'bv'); + assert.strictEqual(map.first, 'av'); + assert.strictEqual(map.last, 'bv'); }); test('LinkedMap - Touch Old one', () => { @@ -77,7 +77,7 @@ suite('Map', () => { test('LinkedMap - basics', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', '2'); @@ -89,23 +89,23 @@ suite('Map', () => { const date = Date.now(); map.set('5', date); - assert.equal(map.size, 5); - assert.equal(map.get('1'), 1); - assert.equal(map.get('2'), '2'); - assert.equal(map.get('3'), true); - assert.equal(map.get('4'), obj); - assert.equal(map.get('5'), date); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.get('2'), '2'); + assert.strictEqual(map.get('3'), true); + assert.strictEqual(map.get('4'), obj); + assert.strictEqual(map.get('5'), date); assert.ok(!map.get('6')); map.delete('6'); - assert.equal(map.size, 5); - assert.equal(map.delete('1'), true); - assert.equal(map.delete('2'), true); - assert.equal(map.delete('3'), true); - assert.equal(map.delete('4'), true); - assert.equal(map.delete('5'), true); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.delete('1'), true); + assert.strictEqual(map.delete('2'), true); + assert.strictEqual(map.delete('3'), true); + assert.strictEqual(map.delete('4'), true); + assert.strictEqual(map.delete('5'), true); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get('5')); assert.ok(!map.get('4')); assert.ok(!map.get('3')); @@ -117,13 +117,13 @@ suite('Map', () => { map.set('3', true); assert.ok(map.has('1')); - assert.equal(map.get('1'), 1); - assert.equal(map.get('2'), '2'); - assert.equal(map.get('3'), true); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.get('2'), '2'); + assert.strictEqual(map.get('3'), true); map.clear(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get('1')); assert.ok(!map.get('2')); assert.ok(!map.get('3')); @@ -231,7 +231,7 @@ suite('Map', () => { for (let i = 11; i <= 20; i++) { cache.set(i, i); } - assert.deepEqual(cache.size, 15); + assert.deepStrictEqual(cache.size, 15); let values: number[] = []; for (let i = 6; i <= 20; i++) { values.push(cache.get(i)!); @@ -269,14 +269,14 @@ suite('Map', () => { let i = 0; map.forEach((value, key) => { if (i === 0) { - assert.equal(key, 'ak'); - assert.equal(value, 'av'); + assert.strictEqual(key, 'ak'); + assert.strictEqual(value, 'av'); } else if (i === 1) { - assert.equal(key, 'bk'); - assert.equal(value, 'bv'); + assert.strictEqual(key, 'bk'); + assert.strictEqual(value, 'bv'); } else if (i === 2) { - assert.equal(key, 'ck'); - assert.equal(value, 'cv'); + assert.strictEqual(key, 'ck'); + assert.strictEqual(value, 'cv'); } i++; }); @@ -285,44 +285,44 @@ suite('Map', () => { test('LinkedMap - delete Head and Tail', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); - assert.equal(map.size, 1); + assert.strictEqual(map.size, 1); map.delete('1'); - assert.equal(map.get('1'), undefined); - assert.equal(map.size, 0); - assert.equal([...map.keys()].length, 0); + assert.strictEqual(map.get('1'), undefined); + assert.strictEqual(map.size, 0); + assert.strictEqual([...map.keys()].length, 0); }); test('LinkedMap - delete Head', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', 2); - assert.equal(map.size, 2); + assert.strictEqual(map.size, 2); map.delete('1'); - assert.equal(map.get('2'), 2); - assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 2); + assert.strictEqual(map.get('2'), 2); + assert.strictEqual(map.size, 1); + assert.strictEqual([...map.keys()].length, 1); + assert.strictEqual([...map.keys()][0], '2'); }); test('LinkedMap - delete Tail', function () { const map = new LinkedMap(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); map.set('1', 1); map.set('2', 2); - assert.equal(map.size, 2); + assert.strictEqual(map.size, 2); map.delete('2'); - assert.equal(map.get('1'), 1); - assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 1); + assert.strictEqual(map.get('1'), 1); + assert.strictEqual(map.size, 1); + assert.strictEqual([...map.keys()].length, 1); + assert.strictEqual([...map.keys()][0], '1'); }); @@ -330,100 +330,100 @@ suite('Map', () => { const iter = new PathIterator(); iter.reset('file:///usr/bin/file.txt'); - assert.equal(iter.value(), 'file:'); - assert.equal(iter.hasNext(), true); - assert.equal(iter.cmp('file:'), 0); + assert.strictEqual(iter.value(), 'file:'); + assert.strictEqual(iter.hasNext(), true); + assert.strictEqual(iter.cmp('file:'), 0); assert.ok(iter.cmp('a') < 0); assert.ok(iter.cmp('aile:') < 0); assert.ok(iter.cmp('z') > 0); assert.ok(iter.cmp('zile:') > 0); iter.next(); - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); iter.next(); - assert.equal(iter.value(), ''); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), ''); + assert.strictEqual(iter.hasNext(), false); iter.next(); - assert.equal(iter.value(), ''); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), ''); + assert.strictEqual(iter.hasNext(), false); // iter.reset('/foo/bar/'); - assert.equal(iter.value(), 'foo'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'foo'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bar'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'bar'); + assert.strictEqual(iter.hasNext(), false); }); test('URIIterator', function () { const iter = new UriIterator(() => false); iter.reset(URI.parse('file:///usr/bin/file.txt')); - assert.equal(iter.value(), 'file'); - // assert.equal(iter.cmp('FILE'), 0); - assert.equal(iter.cmp('file'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); iter.reset(URI.parse('file://share/usr/bin/file.txt?foo')); // scheme - assert.equal(iter.value(), 'file'); - // assert.equal(iter.cmp('FILE'), 0); - assert.equal(iter.cmp('file'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); // authority - assert.equal(iter.value(), 'share'); - assert.equal(iter.cmp('SHARe'), 0); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'share'); + assert.strictEqual(iter.cmp('SHARe'), 0); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'usr'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'bin'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // path - assert.equal(iter.value(), 'file.txt'); - assert.equal(iter.hasNext(), true); + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), true); iter.next(); // query - assert.equal(iter.value(), 'foo'); - assert.equal(iter.cmp('z') > 0, true); - assert.equal(iter.cmp('a') < 0, true); - assert.equal(iter.hasNext(), false); + assert.strictEqual(iter.value(), 'foo'); + assert.strictEqual(iter.cmp('z') > 0, true); + assert.strictEqual(iter.cmp('a') < 0, true); + assert.strictEqual(iter.hasNext(), false); }); function assertTernarySearchTree(trie: TernarySearchTree, ...elements: [string, E][]) { @@ -432,24 +432,24 @@ suite('Map', () => { map.set(key, value); } map.forEach((value, key) => { - assert.equal(trie.get(key), value); + assert.strictEqual(trie.get(key), value); }); // forEach let forEachCount = 0; trie.forEach((element, key) => { - assert.equal(element, map.get(key)); + assert.strictEqual(element, map.get(key)); forEachCount++; }); - assert.equal(map.size, forEachCount); + assert.strictEqual(map.size, forEachCount); // iterator let iterCount = 0; for (let [key, value] of trie) { - assert.equal(value, map.get(key)); + assert.strictEqual(value, map.get(key)); iterCount++; } - assert.equal(map.size, iterCount); + assert.strictEqual(map.size, iterCount); } test('TernarySearchTree - set', function () { @@ -493,13 +493,13 @@ suite('Map', () => { trie.set('foobar', 2); trie.set('foobaz', 3); - assert.equal(trie.findSubstr('f'), undefined); - assert.equal(trie.findSubstr('z'), undefined); - assert.equal(trie.findSubstr('foo'), 1); - assert.equal(trie.findSubstr('fooö'), 1); - assert.equal(trie.findSubstr('fooba'), 1); - assert.equal(trie.findSubstr('foobarr'), 2); - assert.equal(trie.findSubstr('foobazrr'), 3); + assert.strictEqual(trie.findSubstr('f'), undefined); + assert.strictEqual(trie.findSubstr('z'), undefined); + assert.strictEqual(trie.findSubstr('foo'), 1); + assert.strictEqual(trie.findSubstr('fooö'), 1); + assert.strictEqual(trie.findSubstr('fooba'), 1); + assert.strictEqual(trie.findSubstr('foobarr'), 2); + assert.strictEqual(trie.findSubstr('foobazrr'), 3); }); test('TernarySearchTree - basics', function () { @@ -509,27 +509,27 @@ suite('Map', () => { trie.set('bar', 2); trie.set('foobar', 3); - assert.equal(trie.get('foo'), 1); - assert.equal(trie.get('bar'), 2); - assert.equal(trie.get('foobar'), 3); - assert.equal(trie.get('foobaz'), undefined); - assert.equal(trie.get('foobarr'), undefined); + assert.strictEqual(trie.get('foo'), 1); + assert.strictEqual(trie.get('bar'), 2); + assert.strictEqual(trie.get('foobar'), 3); + assert.strictEqual(trie.get('foobaz'), undefined); + assert.strictEqual(trie.get('foobarr'), undefined); - assert.equal(trie.findSubstr('fo'), undefined); - assert.equal(trie.findSubstr('foo'), 1); - assert.equal(trie.findSubstr('foooo'), 1); + assert.strictEqual(trie.findSubstr('fo'), undefined); + assert.strictEqual(trie.findSubstr('foo'), 1); + assert.strictEqual(trie.findSubstr('foooo'), 1); trie.delete('foobar'); trie.delete('bar'); - assert.equal(trie.get('foobar'), undefined); - assert.equal(trie.get('bar'), undefined); + assert.strictEqual(trie.get('foobar'), undefined); + assert.strictEqual(trie.get('bar'), undefined); trie.set('foobar', 17); trie.set('barr', 18); - assert.equal(trie.get('foobar'), 17); - assert.equal(trie.get('barr'), 18); - assert.equal(trie.get('bar'), undefined); + assert.strictEqual(trie.get('foobar'), 17); + assert.strictEqual(trie.get('barr'), 18); + assert.strictEqual(trie.get('bar'), undefined); }); test('TernarySearchTree - delete & cleanup', function () { @@ -576,20 +576,20 @@ suite('Map', () => { trie.set('/user/foo', 2); trie.set('/user/foo/flip/flop', 3); - assert.equal(trie.get('/user/foo/bar'), 1); - assert.equal(trie.get('/user/foo'), 2); - assert.equal(trie.get('/user//foo'), 2); - assert.equal(trie.get('/user\\foo'), 2); - assert.equal(trie.get('/user/foo/flip/flop'), 3); + assert.strictEqual(trie.get('/user/foo/bar'), 1); + assert.strictEqual(trie.get('/user/foo'), 2); + assert.strictEqual(trie.get('/user//foo'), 2); + assert.strictEqual(trie.get('/user\\foo'), 2); + assert.strictEqual(trie.get('/user/foo/flip/flop'), 3); - assert.equal(trie.findSubstr('/user/bar'), undefined); - assert.equal(trie.findSubstr('/user/foo'), 2); - assert.equal(trie.findSubstr('\\user\\foo'), 2); - assert.equal(trie.findSubstr('/user//foo'), 2); - assert.equal(trie.findSubstr('/user/foo/ba'), 2); - assert.equal(trie.findSubstr('/user/foo/far/boo'), 2); - assert.equal(trie.findSubstr('/user/foo/bar'), 1); - assert.equal(trie.findSubstr('/user/foo/bar/far/boo'), 1); + assert.strictEqual(trie.findSubstr('/user/bar'), undefined); + assert.strictEqual(trie.findSubstr('/user/foo'), 2); + assert.strictEqual(trie.findSubstr('\\user\\foo'), 2); + assert.strictEqual(trie.findSubstr('/user//foo'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/ba'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/far/boo'), 2); + assert.strictEqual(trie.findSubstr('/user/foo/bar'), 1); + assert.strictEqual(trie.findSubstr('/user/foo/bar/far/boo'), 1); }); test('TernarySearchTree (PathSegments) - lookup', function () { @@ -599,11 +599,11 @@ suite('Map', () => { map.set('/user/foo', 2); map.set('/user/foo/flip/flop', 3); - assert.equal(map.get('/foo'), undefined); - assert.equal(map.get('/user'), undefined); - assert.equal(map.get('/user/foo'), 2); - assert.equal(map.get('/user/foo/bar'), 1); - assert.equal(map.get('/user/foo/bar/boo'), undefined); + assert.strictEqual(map.get('/foo'), undefined); + assert.strictEqual(map.get('/user'), undefined); + assert.strictEqual(map.get('/user/foo'), 2); + assert.strictEqual(map.get('/user/foo/bar'), 1); + assert.strictEqual(map.get('/user/foo/bar/boo'), undefined); }); test('TernarySearchTree (PathSegments) - superstr', function () { @@ -618,31 +618,31 @@ suite('Map', () => { let iter = map.findSuperstr('/user'); item = iter!.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr('/usr'); item = iter!.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr('/not'), undefined); - assert.equal(map.findSuperstr('/us'), undefined); - assert.equal(map.findSuperstr('/usrr'), undefined); - assert.equal(map.findSuperstr('/userr'), undefined); + assert.strictEqual(map.findSuperstr('/not'), undefined); + assert.strictEqual(map.findSuperstr('/us'), undefined); + assert.strictEqual(map.findSuperstr('/usrr'), undefined); + assert.strictEqual(map.findSuperstr('/userr'), undefined); }); @@ -688,16 +688,16 @@ suite('Map', () => { trie.set(URI.file('/user/foo'), 2); trie.set(URI.file('/user/foo/flip/flop'), 3); - assert.equal(trie.get(URI.file('/user/foo/bar')), 1); - assert.equal(trie.get(URI.file('/user/foo')), 2); - assert.equal(trie.get(URI.file('/user/foo/flip/flop')), 3); + assert.strictEqual(trie.get(URI.file('/user/foo/bar')), 1); + assert.strictEqual(trie.get(URI.file('/user/foo')), 2); + assert.strictEqual(trie.get(URI.file('/user/foo/flip/flop')), 3); - assert.equal(trie.findSubstr(URI.file('/user/bar')), undefined); - assert.equal(trie.findSubstr(URI.file('/user/foo')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/ba')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/far/boo')), 2); - assert.equal(trie.findSubstr(URI.file('/user/foo/bar')), 1); - assert.equal(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); + assert.strictEqual(trie.findSubstr(URI.file('/user/bar')), undefined); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/ba')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/far/boo')), 2); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar')), 1); + assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); }); test('TernarySearchTree (URI) - lookup', function () { @@ -708,23 +708,23 @@ suite('Map', () => { map.set(URI.parse('http://foo.bar/user/foo?QUERY'), 3); map.set(URI.parse('http://foo.bar/user/foo/flip/flop'), 3); - assert.equal(map.get(URI.parse('http://foo.bar/foo')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar')), 1); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?query')), 2); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?Query')), undefined); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo?QUERY')), 3); - assert.equal(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/foo')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo/bar')), 1); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?query')), 2); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?Query')), undefined); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo?QUERY')), 3); + assert.strictEqual(map.get(URI.parse('http://foo.bar/user/foo/bar/boo')), undefined); }); test('TernarySearchTree (URI) - lookup, casing', function () { const map = new TernarySearchTree(new UriIterator(uri => /^https?$/.test(uri.scheme))); map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); - assert.equal(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); + assert.strictEqual(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); map.set(URI.parse('foo://foo.bar/user/foo/bar'), 1); - assert.equal(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); + assert.strictEqual(map.get(URI.parse('foo://foo.bar/USER/foo/bar')), undefined); }); test('TernarySearchTree (URI) - superstr', function () { @@ -739,48 +739,48 @@ suite('Map', () => { let iter = map.findSuperstr(URI.file('/user'))!; item = iter.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr(URI.file('/usr'))!; item = iter.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); iter = map.findSuperstr(URI.file('/'))!; item = iter.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value[1], 4); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 4); + assert.strictEqual(item.done, false); item = iter.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr(URI.file('/not')), undefined); - assert.equal(map.findSuperstr(URI.file('/us')), undefined); - assert.equal(map.findSuperstr(URI.file('/usrr')), undefined); - assert.equal(map.findSuperstr(URI.file('/userr')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/not')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/us')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/usrr')), undefined); + assert.strictEqual(map.findSuperstr(URI.file('/userr')), undefined); }); test('TernarySearchTree (ConfigKeySegments) - basics', function () { @@ -790,16 +790,16 @@ suite('Map', () => { trie.set('config.foo', 2); trie.set('config.foo.flip.flop', 3); - assert.equal(trie.get('config.foo.bar'), 1); - assert.equal(trie.get('config.foo'), 2); - assert.equal(trie.get('config.foo.flip.flop'), 3); + assert.strictEqual(trie.get('config.foo.bar'), 1); + assert.strictEqual(trie.get('config.foo'), 2); + assert.strictEqual(trie.get('config.foo.flip.flop'), 3); - assert.equal(trie.findSubstr('config.bar'), undefined); - assert.equal(trie.findSubstr('config.foo'), 2); - assert.equal(trie.findSubstr('config.foo.ba'), 2); - assert.equal(trie.findSubstr('config.foo.far.boo'), 2); - assert.equal(trie.findSubstr('config.foo.bar'), 1); - assert.equal(trie.findSubstr('config.foo.bar.far.boo'), 1); + assert.strictEqual(trie.findSubstr('config.bar'), undefined); + assert.strictEqual(trie.findSubstr('config.foo'), 2); + assert.strictEqual(trie.findSubstr('config.foo.ba'), 2); + assert.strictEqual(trie.findSubstr('config.foo.far.boo'), 2); + assert.strictEqual(trie.findSubstr('config.foo.bar'), 1); + assert.strictEqual(trie.findSubstr('config.foo.bar.far.boo'), 1); }); test('TernarySearchTree (ConfigKeySegments) - lookup', function () { @@ -809,11 +809,11 @@ suite('Map', () => { map.set('config.foo', 2); map.set('config.foo.flip.flop', 3); - assert.equal(map.get('foo'), undefined); - assert.equal(map.get('config'), undefined); - assert.equal(map.get('config.foo'), 2); - assert.equal(map.get('config.foo.bar'), 1); - assert.equal(map.get('config.foo.bar.boo'), undefined); + assert.strictEqual(map.get('foo'), undefined); + assert.strictEqual(map.get('config'), undefined); + assert.strictEqual(map.get('config.foo'), 2); + assert.strictEqual(map.get('config.foo.bar'), 1); + assert.strictEqual(map.get('config.foo.bar.boo'), undefined); }); test('TernarySearchTree (ConfigKeySegments) - superstr', function () { @@ -828,21 +828,21 @@ suite('Map', () => { let iter = map.findSuperstr('config'); item = iter!.next(); - assert.equal(item.value[1], 2); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 2); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 1); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 1); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value[1], 3); - assert.equal(item.done, false); + assert.strictEqual(item.value[1], 3); + assert.strictEqual(item.done, false); item = iter!.next(); - assert.equal(item.value, undefined); - assert.equal(item.done, true); + assert.strictEqual(item.value, undefined); + assert.strictEqual(item.done, true); - assert.equal(map.findSuperstr('foo'), undefined); - assert.equal(map.findSuperstr('config.foo.no'), undefined); - assert.equal(map.findSuperstr('config.foop'), undefined); + assert.strictEqual(map.findSuperstr('foo'), undefined); + assert.strictEqual(map.findSuperstr('config.foo.no'), undefined); + assert.strictEqual(map.findSuperstr('config.foop'), undefined); }); @@ -891,7 +891,7 @@ suite('Map', () => { const resource5 = URI.parse('some://5'); const resource6 = URI.parse('some://6'); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); let res = map.set(resource1, 1); assert.ok(res === map); @@ -899,13 +899,13 @@ suite('Map', () => { map.set(resource3, true); const values = [...map.values()]; - assert.equal(values[0], 1); - assert.equal(values[1], '2'); - assert.equal(values[2], true); + assert.strictEqual(values[0], 1); + assert.strictEqual(values[1], '2'); + assert.strictEqual(values[2], true); let counter = 0; map.forEach((value, key, mapObj) => { - assert.equal(value, values[counter++]); + assert.strictEqual(value, values[counter++]); assert.ok(URI.isUri(key)); assert.ok(map === mapObj); }); @@ -916,23 +916,23 @@ suite('Map', () => { const date = Date.now(); map.set(resource5, date); - assert.equal(map.size, 5); - assert.equal(map.get(resource1), 1); - assert.equal(map.get(resource2), '2'); - assert.equal(map.get(resource3), true); - assert.equal(map.get(resource4), obj); - assert.equal(map.get(resource5), date); + assert.strictEqual(map.size, 5); + assert.strictEqual(map.get(resource1), 1); + assert.strictEqual(map.get(resource2), '2'); + assert.strictEqual(map.get(resource3), true); + assert.strictEqual(map.get(resource4), obj); + assert.strictEqual(map.get(resource5), date); assert.ok(!map.get(resource6)); map.delete(resource6); - assert.equal(map.size, 5); + assert.strictEqual(map.size, 5); assert.ok(map.delete(resource1)); assert.ok(map.delete(resource2)); assert.ok(map.delete(resource3)); assert.ok(map.delete(resource4)); assert.ok(map.delete(resource5)); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get(resource5)); assert.ok(!map.get(resource4)); assert.ok(!map.get(resource3)); @@ -944,13 +944,13 @@ suite('Map', () => { map.set(resource3, true); assert.ok(map.has(resource1)); - assert.equal(map.get(resource1), 1); - assert.equal(map.get(resource2), '2'); - assert.equal(map.get(resource3), true); + assert.strictEqual(map.get(resource1), 1); + assert.strictEqual(map.get(resource2), '2'); + assert.strictEqual(map.get(resource3), true); map.clear(); - assert.equal(map.size, 0); + assert.strictEqual(map.size, 0); assert.ok(!map.get(resource1)); assert.ok(!map.get(resource2)); assert.ok(!map.get(resource3)); @@ -971,16 +971,16 @@ suite('Map', () => { const fileAUpper = URI.parse('file://SOME/FILEA'); map.set(fileA, 'true'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); assert.ok(!map.get(fileAUpper)); assert.ok(!map.get(fileB)); map.set(fileAUpper, 'false'); - assert.equal(map.get(fileAUpper), 'false'); + assert.strictEqual(map.get(fileAUpper), 'false'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); const windowsFile = URI.file('c:\\test with %25\\c#code'); const uncFile = URI.file('\\\\shäres\\path\\c#\\plugin.json'); @@ -988,8 +988,8 @@ suite('Map', () => { map.set(windowsFile, 'true'); map.set(uncFile, 'true'); - assert.equal(map.get(windowsFile), 'true'); - assert.equal(map.get(uncFile), 'true'); + assert.strictEqual(map.get(windowsFile), 'true'); + assert.strictEqual(map.get(uncFile), 'true'); }); test('ResourceMap - files (ignorecase)', function () { @@ -1000,16 +1000,16 @@ suite('Map', () => { const fileAUpper = URI.parse('file://SOME/FILEA'); map.set(fileA, 'true'); - assert.equal(map.get(fileA), 'true'); + assert.strictEqual(map.get(fileA), 'true'); - assert.equal(map.get(fileAUpper), 'true'); + assert.strictEqual(map.get(fileAUpper), 'true'); assert.ok(!map.get(fileB)); map.set(fileAUpper, 'false'); - assert.equal(map.get(fileAUpper), 'false'); + assert.strictEqual(map.get(fileAUpper), 'false'); - assert.equal(map.get(fileA), 'false'); + assert.strictEqual(map.get(fileA), 'false'); const windowsFile = URI.file('c:\\test with %25\\c#code'); const uncFile = URI.file('\\\\shäres\\path\\c#\\plugin.json'); @@ -1017,7 +1017,7 @@ suite('Map', () => { map.set(windowsFile, 'true'); map.set(uncFile, 'true'); - assert.equal(map.get(windowsFile), 'true'); - assert.equal(map.get(uncFile), 'true'); + assert.strictEqual(map.get(windowsFile), 'true'); + assert.strictEqual(map.get(uncFile), 'true'); }); }); diff --git a/src/vs/base/test/common/markdownString.test.ts b/src/vs/base/test/common/markdownString.test.ts index 71021355a4..27caa3c36d 100644 --- a/src/vs/base/test/common/markdownString.test.ts +++ b/src/vs/base/test/common/markdownString.test.ts @@ -11,13 +11,13 @@ suite('MarkdownString', () => { test('Escape leading whitespace', function () { const mds = new MarkdownString(); mds.appendText('Hello\n Not a code block'); - assert.equal(mds.value, 'Hello\n\n    Not a code block'); + assert.strictEqual(mds.value, 'Hello\n\n    Not a code block'); }); test('MarkdownString.appendText doesn\'t escape quote #109040', function () { const mds = new MarkdownString(); mds.appendText('> Text\n>More'); - assert.equal(mds.value, '\\> Text\n\n\\>More'); + assert.strictEqual(mds.value, '\\> Text\n\n\\>More'); }); test('appendText', () => { @@ -25,7 +25,7 @@ suite('MarkdownString', () => { const mds = new MarkdownString(); mds.appendText('# foo\n*bar*'); - assert.equal(mds.value, '\\# foo\n\n\\*bar\\*'); + assert.strictEqual(mds.value, '\\# foo\n\n\\*bar\\*'); }); suite('ThemeIcons', () => { @@ -36,21 +36,21 @@ suite('MarkdownString', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\\\$\\(zap\\) $\\(not a theme icon\\) \\\\$\\(add\\)'); + assert.strictEqual(mds.value, '\\\\$\\(zap\\) $\\(not a theme icon\\) \\\\$\\(add\\)'); }); test('appendMarkdown', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '$(zap) $(not a theme icon) $(add)'); }); test('appendMarkdown with escaped icon', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '\\$(zap) $(not a theme icon) $(add)'); }); }); @@ -61,21 +61,21 @@ suite('MarkdownString', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendText('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$\\(zap\\) $\\(not a theme icon\\) $\\(add\\)'); + assert.strictEqual(mds.value, '$\\(zap\\) $\\(not a theme icon\\) $\\(add\\)'); }); test('appendMarkdown', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: false }); mds.appendMarkdown('$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '$(zap) $(not a theme icon) $(add)'); }); test('appendMarkdown with escaped icon', () => { const mds = new MarkdownString(undefined, { supportThemeIcons: true }); mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)'); - assert.equal(mds.value, '\\$(zap) $(not a theme icon) $(add)'); + assert.strictEqual(mds.value, '\\$(zap) $(not a theme icon) $(add)'); }); }); diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index 2a9d335be4..cbf4f0ff8d 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -11,38 +11,38 @@ suite('Mime', () => { test('Dynamically Register Text Mime', () => { let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); registerTextMime({ id: 'monaco', extension: '.monaco', mime: 'text/monaco' }); guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); registerTextMime({ id: 'codefile', filename: 'Codefile', mime: 'text/code' }); guess = guessMimeTypes(URI.file('Codefile')); - assert.deepEqual(guess, ['text/code', 'text/plain']); + assert.deepStrictEqual(guess, ['text/code', 'text/plain']); guess = guessMimeTypes(URI.file('foo.Codefile')); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); registerTextMime({ id: 'docker', filepattern: 'Docker*', mime: 'text/docker' }); guess = guessMimeTypes(URI.file('Docker-debug')); - assert.deepEqual(guess, ['text/docker', 'text/plain']); + assert.deepStrictEqual(guess, ['text/docker', 'text/plain']); guess = guessMimeTypes(URI.file('docker-PROD')); - assert.deepEqual(guess, ['text/docker', 'text/plain']); + assert.deepStrictEqual(guess, ['text/docker', 'text/plain']); registerTextMime({ id: 'niceregex', mime: 'text/nice-regex', firstline: /RegexesAreNice/ }); guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNice'); - assert.deepEqual(guess, ['text/nice-regex', 'text/plain']); + assert.deepStrictEqual(guess, ['text/nice-regex', 'text/plain']); guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNotNice'); - assert.deepEqual(guess, ['application/unknown']); + assert.deepStrictEqual(guess, ['application/unknown']); guess = guessMimeTypes(URI.file('Codefile'), 'RegexesAreNice'); - assert.deepEqual(guess, ['text/code', 'text/plain']); + assert.deepStrictEqual(guess, ['text/code', 'text/plain']); }); test('Mimes Priority', () => { @@ -50,36 +50,36 @@ suite('Mime', () => { registerTextMime({ id: 'foobar', mime: 'text/foobar', firstline: /foobar/ }); let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco'), 'foobar'); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); registerTextMime({ id: 'docker', filename: 'dockerfile', mime: 'text/winner' }); registerTextMime({ id: 'docker', filepattern: 'dockerfile*', mime: 'text/looser' }); guess = guessMimeTypes(URI.file('dockerfile')); - assert.deepEqual(guess, ['text/winner', 'text/plain']); + assert.deepStrictEqual(guess, ['text/winner', 'text/plain']); registerTextMime({ id: 'azure-looser', mime: 'text/azure-looser', firstline: /azure/ }); registerTextMime({ id: 'azure-winner', mime: 'text/azure-winner', firstline: /azure/ }); guess = guessMimeTypes(URI.file('azure'), 'azure'); - assert.deepEqual(guess, ['text/azure-winner', 'text/plain']); + assert.deepStrictEqual(guess, ['text/azure-winner', 'text/plain']); }); test('Specificity priority 1', () => { registerTextMime({ id: 'monaco2', extension: '.monaco2', mime: 'text/monaco2' }); registerTextMime({ id: 'monaco2', filename: 'specific.monaco2', mime: 'text/specific-monaco2' }); - assert.deepEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']); - assert.deepEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']); }); test('Specificity priority 2', () => { registerTextMime({ id: 'monaco3', filename: 'specific.monaco3', mime: 'text/specific-monaco3' }); registerTextMime({ id: 'monaco3', extension: '.monaco3', mime: 'text/monaco3' }); - assert.deepEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']); - assert.deepEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']); }); test('Mimes Priority - Longest Extension wins', () => { @@ -88,13 +88,13 @@ suite('Mime', () => { registerTextMime({ id: 'monaco', extension: '.monaco.xml.build', mime: 'text/monaco-xml-build' }); let guess = guessMimeTypes(URI.file('foo.monaco')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco.xml')); - assert.deepEqual(guess, ['text/monaco-xml', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco-xml', 'text/plain']); guess = guessMimeTypes(URI.file('foo.monaco.xml.build')); - assert.deepEqual(guess, ['text/monaco-xml-build', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco-xml-build', 'text/plain']); }); test('Mimes Priority - User configured wins', () => { @@ -102,7 +102,7 @@ suite('Mime', () => { registerTextMime({ id: 'monaco', extension: '.monaco.xml', mime: 'text/monaco-xml' }); let guess = guessMimeTypes(URI.file('foo.monaco.xnl')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); }); test('Mimes Priority - Pattern matches on path if specified', () => { @@ -110,7 +110,7 @@ suite('Mime', () => { registerTextMime({ id: 'other', filepattern: '*ot.other.xml', mime: 'text/other' }); let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml')); - assert.deepEqual(guess, ['text/monaco', 'text/plain']); + assert.deepStrictEqual(guess, ['text/monaco', 'text/plain']); }); test('Mimes Priority - Last registered mime wins', () => { @@ -118,12 +118,12 @@ suite('Mime', () => { registerTextMime({ id: 'other', filepattern: '**/dot.monaco.xml', mime: 'text/other' }); let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml')); - assert.deepEqual(guess, ['text/other', 'text/plain']); + assert.deepStrictEqual(guess, ['text/other', 'text/plain']); }); test('Data URIs', () => { registerTextMime({ id: 'data', extension: '.data', mime: 'text/data' }); - assert.deepEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); + assert.deepStrictEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); }); diff --git a/src/vs/base/test/common/network.test.ts b/src/vs/base/test/common/network.test.ts index b79472bfc0..2bf90a30b8 100644 --- a/src/vs/base/test/common/network.test.ts +++ b/src/vs/base/test/common/network.test.ts @@ -7,10 +7,10 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { isElectronSandboxed } from 'vs/base/common/platform'; +import { isPreferringBrowserCodeLoad } from 'vs/base/common/platform'; suite('network', () => { - const enableTest = isElectronSandboxed; + const enableTest = isPreferringBrowserCodeLoad; (!enableTest ? test.skip : test)('FileAccess: URI (native)', () => { @@ -19,52 +19,52 @@ suite('network', () => { let browserUri = FileAccess.asBrowserUri(originalFileUri); assert.ok(browserUri.authority.length > 0); let fileUri = FileAccess.asFileUri(browserUri); - assert.equal(fileUri.authority.length, 0); + assert.strictEqual(fileUri.authority.length, 0); assert(isEqual(originalFileUri, fileUri)); // asCodeUri() & asFileUri(): with authority originalFileUri = URI.file('network.test.ts').with({ authority: 'test-authority' }); browserUri = FileAccess.asBrowserUri(originalFileUri); - assert.equal(browserUri.authority, originalFileUri.authority); + assert.strictEqual(browserUri.authority, originalFileUri.authority); fileUri = FileAccess.asFileUri(browserUri); assert(isEqual(originalFileUri, fileUri)); }); (!enableTest ? test.skip : test)('FileAccess: moduleId (native)', () => { const browserUri = FileAccess.asBrowserUri('vs/base/test/node/network.test', require); - assert.equal(browserUri.scheme, Schemas.vscodeFileResource); + assert.strictEqual(browserUri.scheme, Schemas.vscodeFileResource); const fileUri = FileAccess.asFileUri('vs/base/test/node/network.test', require); - assert.equal(fileUri.scheme, Schemas.file); + assert.strictEqual(fileUri.scheme, Schemas.file); }); (!enableTest ? test.skip : test)('FileAccess: query and fragment is dropped (native)', () => { let originalFileUri = URI.file('network.test.ts').with({ query: 'foo=bar', fragment: 'something' }); let browserUri = FileAccess.asBrowserUri(originalFileUri); - assert.equal(browserUri.query, ''); - assert.equal(browserUri.fragment, ''); + assert.strictEqual(browserUri.query, ''); + assert.strictEqual(browserUri.fragment, ''); }); (!enableTest ? test.skip : test)('FileAccess: query and fragment is kept if URI is already of same scheme (native)', () => { let originalFileUri = URI.file('network.test.ts').with({ query: 'foo=bar', fragment: 'something' }); let browserUri = FileAccess.asBrowserUri(originalFileUri.with({ scheme: Schemas.vscodeFileResource })); - assert.equal(browserUri.query, 'foo=bar'); - assert.equal(browserUri.fragment, 'something'); + assert.strictEqual(browserUri.query, 'foo=bar'); + assert.strictEqual(browserUri.fragment, 'something'); let fileUri = FileAccess.asFileUri(originalFileUri); - assert.equal(fileUri.query, 'foo=bar'); - assert.equal(fileUri.fragment, 'something'); + assert.strictEqual(fileUri.query, 'foo=bar'); + assert.strictEqual(fileUri.fragment, 'something'); }); (!enableTest ? test.skip : test)('FileAccess: web', () => { const originalHttpsUri = URI.file('network.test.ts').with({ scheme: 'https' }); const browserUri = FileAccess.asBrowserUri(originalHttpsUri); - assert.equal(originalHttpsUri.toString(), browserUri.toString()); + assert.strictEqual(originalHttpsUri.toString(), browserUri.toString()); }); test('FileAccess: remote URIs', () => { const originalRemoteUri = URI.file('network.test.ts').with({ scheme: Schemas.vscodeRemote }); const browserUri = FileAccess.asBrowserUri(originalRemoteUri); - assert.notEqual(originalRemoteUri.scheme, browserUri.scheme); + assert.notStrictEqual(originalRemoteUri.scheme, browserUri.scheme); }); }); diff --git a/src/vs/base/test/common/paging.test.ts b/src/vs/base/test/common/paging.test.ts index 0fe96e7983..0dfb7fb381 100644 --- a/src/vs/base/test/common/paging.test.ts +++ b/src/vs/base/test/common/paging.test.ts @@ -101,7 +101,6 @@ suite('PagedModel', () => { test('preemptive cancellation works', async function () { const pager = new TestPager(() => { assert(false); - return Promise.resolve([]); }); const model = new PagedModel(pager); diff --git a/src/vs/base/test/node/path.test.ts b/src/vs/base/test/common/path.test.ts similarity index 99% rename from src/vs/base/test/node/path.test.ts rename to src/vs/base/test/common/path.test.ts index 0b2a975578..bd182b329e 100644 --- a/src/vs/base/test/node/path.test.ts +++ b/src/vs/base/test/common/path.test.ts @@ -29,10 +29,11 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { isWindows } from 'vs/base/common/platform'; +import { isWeb, isWindows } from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; suite('Paths (Node Implementation)', () => { + const __filename = 'path.test.js'; test('join', () => { const failures = [] as string[]; const backslashRE = /\\/g; @@ -175,9 +176,6 @@ suite('Paths (Node Implementation)', () => { }); test('dirname', () => { - assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-9), - isWindows ? 'test\\node' : 'test/node'); - assert.strictEqual(path.posix.dirname('/a/b/'), '/a'); assert.strictEqual(path.posix.dirname('/a/b'), '/a'); assert.strictEqual(path.posix.dirname('/a'), '/'); @@ -362,7 +360,7 @@ suite('Paths (Node Implementation)', () => { assert.equal(path.extname('far.boo/boo'), ''); }); - test('resolve', () => { + (isWeb && isWindows ? test.skip : test)('resolve', () => { // TODO@sbatten fails on windows & browser only const failures = [] as string[]; const slashRE = /\//g; const backslashRE = /\\/g; diff --git a/src/vs/base/test/common/processes.test.ts b/src/vs/base/test/common/processes.test.ts index ef313ff198..10e40b7f62 100644 --- a/src/vs/base/test/common/processes.test.ts +++ b/src/vs/base/test/common/processes.test.ts @@ -15,7 +15,6 @@ suite('Processes', () => { ELECTRON_NO_ASAR: 'x', ELECTRON_NO_ATTACH_CONSOLE: 'x', ELECTRON_RUN_AS_NODE: 'x', - GOOGLE_API_KEY: 'x', VSCODE_CLI: 'x', VSCODE_DEV: 'x', VSCODE_IPC_HOOK: 'x', diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 40f6ca73c1..9cc588c98c 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -22,10 +22,10 @@ suite('Resources', () => { ]; let distinct = distinctParents(resources, r => r); - assert.equal(distinct.length, 3); - assert.equal(distinct[0].toString(), resources[0].toString()); - assert.equal(distinct[1].toString(), resources[1].toString()); - assert.equal(distinct[2].toString(), resources[2].toString()); + assert.strictEqual(distinct.length, 3); + assert.strictEqual(distinct[0].toString(), resources[0].toString()); + assert.strictEqual(distinct[1].toString(), resources[1].toString()); + assert.strictEqual(distinct[2].toString(), resources[2].toString()); // Parent / Child resources = [ @@ -37,144 +37,144 @@ suite('Resources', () => { ]; distinct = distinctParents(resources, r => r); - assert.equal(distinct.length, 3); - assert.equal(distinct[0].toString(), resources[0].toString()); - assert.equal(distinct[1].toString(), resources[3].toString()); - assert.equal(distinct[2].toString(), resources[4].toString()); + assert.strictEqual(distinct.length, 3); + assert.strictEqual(distinct[0].toString(), resources[0].toString()); + assert.strictEqual(distinct[1].toString(), resources[3].toString()); + assert.strictEqual(distinct[2].toString(), resources[4].toString()); }); test('dirname', () => { if (isWindows) { - assert.equal(dirname(URI.file('c:\\some\\file\\test.txt')).toString(), 'file:///c%3A/some/file'); - assert.equal(dirname(URI.file('c:\\some\\file')).toString(), 'file:///c%3A/some'); - assert.equal(dirname(URI.file('c:\\some\\file\\')).toString(), 'file:///c%3A/some'); - assert.equal(dirname(URI.file('c:\\some')).toString(), 'file:///c%3A/'); - assert.equal(dirname(URI.file('C:\\some')).toString(), 'file:///c%3A/'); - assert.equal(dirname(URI.file('c:\\')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('c:\\some\\file\\test.txt')).toString(), 'file:///c%3A/some/file'); + assert.strictEqual(dirname(URI.file('c:\\some\\file')).toString(), 'file:///c%3A/some'); + assert.strictEqual(dirname(URI.file('c:\\some\\file\\')).toString(), 'file:///c%3A/some'); + assert.strictEqual(dirname(URI.file('c:\\some')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('C:\\some')).toString(), 'file:///c%3A/'); + assert.strictEqual(dirname(URI.file('c:\\')).toString(), 'file:///c%3A/'); } else { - assert.equal(dirname(URI.file('/some/file/test.txt')).toString(), 'file:///some/file'); - assert.equal(dirname(URI.file('/some/file/')).toString(), 'file:///some'); - assert.equal(dirname(URI.file('/some/file')).toString(), 'file:///some'); + assert.strictEqual(dirname(URI.file('/some/file/test.txt')).toString(), 'file:///some/file'); + assert.strictEqual(dirname(URI.file('/some/file/')).toString(), 'file:///some'); + assert.strictEqual(dirname(URI.file('/some/file')).toString(), 'file:///some'); } - assert.equal(dirname(URI.parse('foo://a/some/file/test.txt')).toString(), 'foo://a/some/file'); - assert.equal(dirname(URI.parse('foo://a/some/file/')).toString(), 'foo://a/some'); - assert.equal(dirname(URI.parse('foo://a/some/file')).toString(), 'foo://a/some'); - assert.equal(dirname(URI.parse('foo://a/some')).toString(), 'foo://a/'); - assert.equal(dirname(URI.parse('foo://a/')).toString(), 'foo://a/'); - assert.equal(dirname(URI.parse('foo://a')).toString(), 'foo://a'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file/test.txt')).toString(), 'foo://a/some/file'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file/')).toString(), 'foo://a/some'); + assert.strictEqual(dirname(URI.parse('foo://a/some/file')).toString(), 'foo://a/some'); + assert.strictEqual(dirname(URI.parse('foo://a/some')).toString(), 'foo://a/'); + assert.strictEqual(dirname(URI.parse('foo://a/')).toString(), 'foo://a/'); + assert.strictEqual(dirname(URI.parse('foo://a')).toString(), 'foo://a'); // does not explode (https://github.com/microsoft/vscode/issues/41987) dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' })); - assert.equal(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q'); + assert.strictEqual(dirname(URI.parse('foo://a/b/c?q')).toString(), 'foo://a/b?q'); }); test('basename', () => { if (isWindows) { - assert.equal(basename(URI.file('c:\\some\\file\\test.txt')), 'test.txt'); - assert.equal(basename(URI.file('c:\\some\\file')), 'file'); - assert.equal(basename(URI.file('c:\\some\\file\\')), 'file'); - assert.equal(basename(URI.file('C:\\some\\file\\')), 'file'); + assert.strictEqual(basename(URI.file('c:\\some\\file\\test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.file('c:\\some\\file')), 'file'); + assert.strictEqual(basename(URI.file('c:\\some\\file\\')), 'file'); + assert.strictEqual(basename(URI.file('C:\\some\\file\\')), 'file'); } else { - assert.equal(basename(URI.file('/some/file/test.txt')), 'test.txt'); - assert.equal(basename(URI.file('/some/file/')), 'file'); - assert.equal(basename(URI.file('/some/file')), 'file'); - assert.equal(basename(URI.file('/some')), 'some'); + assert.strictEqual(basename(URI.file('/some/file/test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.file('/some/file/')), 'file'); + assert.strictEqual(basename(URI.file('/some/file')), 'file'); + assert.strictEqual(basename(URI.file('/some')), 'some'); } - assert.equal(basename(URI.parse('foo://a/some/file/test.txt')), 'test.txt'); - assert.equal(basename(URI.parse('foo://a/some/file/')), 'file'); - assert.equal(basename(URI.parse('foo://a/some/file')), 'file'); - assert.equal(basename(URI.parse('foo://a/some')), 'some'); - assert.equal(basename(URI.parse('foo://a/')), ''); - assert.equal(basename(URI.parse('foo://a')), ''); + assert.strictEqual(basename(URI.parse('foo://a/some/file/test.txt')), 'test.txt'); + assert.strictEqual(basename(URI.parse('foo://a/some/file/')), 'file'); + assert.strictEqual(basename(URI.parse('foo://a/some/file')), 'file'); + assert.strictEqual(basename(URI.parse('foo://a/some')), 'some'); + assert.strictEqual(basename(URI.parse('foo://a/')), ''); + assert.strictEqual(basename(URI.parse('foo://a')), ''); }); test('joinPath', () => { if (isWindows) { - assert.equal(joinPath(URI.file('c:\\foo\\bar'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo\\bar\\'), 'file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo\\bar\\'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\'), '/file.js').toString(), 'file:///c%3A/file.js'); - assert.equal(joinPath(URI.file('c:\\'), 'bar/file.js').toString(), 'file:///c%3A/bar/file.js'); - assert.equal(joinPath(URI.file('c:\\foo'), './file.js').toString(), 'file:///c%3A/foo/file.js'); - assert.equal(joinPath(URI.file('c:\\foo'), '/./file.js').toString(), 'file:///c%3A/foo/file.js'); - assert.equal(joinPath(URI.file('C:\\foo'), '../file.js').toString(), 'file:///c%3A/file.js'); - assert.equal(joinPath(URI.file('C:\\foo\\.'), '../file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar\\'), 'file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo\\bar\\'), '/file.js').toString(), 'file:///c%3A/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\'), '/file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\'), 'bar/file.js').toString(), 'file:///c%3A/bar/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo'), './file.js').toString(), 'file:///c%3A/foo/file.js'); + assert.strictEqual(joinPath(URI.file('c:\\foo'), '/./file.js').toString(), 'file:///c%3A/foo/file.js'); + assert.strictEqual(joinPath(URI.file('C:\\foo'), '../file.js').toString(), 'file:///c%3A/file.js'); + assert.strictEqual(joinPath(URI.file('C:\\foo\\.'), '../file.js').toString(), 'file:///c%3A/file.js'); } else { - assert.equal(joinPath(URI.file('/foo/bar'), '/file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), 'file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar/'), '/file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/'), '/file.js').toString(), 'file:///file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), './file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); - assert.equal(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '/file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), 'file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar/'), '/file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/'), '/file.js').toString(), 'file:///file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), './file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '/./file.js').toString(), 'file:///foo/bar/file.js'); + assert.strictEqual(joinPath(URI.file('/foo/bar'), '../file.js').toString(), 'file:///foo/file.js'); } - assert.equal(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/'), '/file.js').toString(), 'foo://a/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), './file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '/./file.js').toString(), 'foo://a/foo/bar/file.js'); - assert.equal(joinPath(URI.parse('foo://a/foo/bar/'), '../file.js').toString(), 'foo://a/foo/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar'), 'file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '/file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/'), '/file.js').toString(), 'foo://a/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), './file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '/./file.js').toString(), 'foo://a/foo/bar/file.js'); + assert.strictEqual(joinPath(URI.parse('foo://a/foo/bar/'), '../file.js').toString(), 'foo://a/foo/file.js'); - assert.equal( + assert.strictEqual( joinPath(URI.from({ scheme: 'myScheme', authority: 'authority', path: '/path', query: 'query', fragment: 'fragment' }), '/file.js').toString(), 'myScheme://authority/path/file.js?query#fragment'); }); test('normalizePath', () => { if (isWindows) { - assert.equal(normalizePath(URI.file('c:\\foo\\.\\bar')).toString(), 'file:///c%3A/foo/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\.')).toString(), 'file:///c%3A/foo'); - assert.equal(normalizePath(URI.file('c:\\foo\\.\\')).toString(), 'file:///c%3A/foo/'); - assert.equal(normalizePath(URI.file('c:\\foo\\..')).toString(), 'file:///c%3A/'); - assert.equal(normalizePath(URI.file('c:\\foo\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('c:\\foo\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); - assert.equal(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\some\\..\\bar')).toString(), 'file:///c%3A/foo/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.\\bar')).toString(), 'file:///c%3A/foo/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.')).toString(), 'file:///c%3A/foo'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\.\\')).toString(), 'file:///c%3A/foo/'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..')).toString(), 'file:///c%3A/'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('c:\\foo\\foo\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\..\\bar')).toString(), 'file:///c%3A/bar'); + assert.strictEqual(normalizePath(URI.file('C:\\foo\\foo\\.\\..\\some\\..\\bar')).toString(), 'file:///c%3A/foo/bar'); } else { - assert.equal(normalizePath(URI.file('/foo/./bar')).toString(), 'file:///foo/bar'); - assert.equal(normalizePath(URI.file('/foo/.')).toString(), 'file:///foo'); - assert.equal(normalizePath(URI.file('/foo/./')).toString(), 'file:///foo/'); - assert.equal(normalizePath(URI.file('/foo/..')).toString(), 'file:///'); - assert.equal(normalizePath(URI.file('/foo/../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/./../../bar')).toString(), 'file:///bar'); - assert.equal(normalizePath(URI.file('/foo/foo/./../some/../bar')).toString(), 'file:///foo/bar'); - assert.equal(normalizePath(URI.file('/f')).toString(), 'file:///f'); + assert.strictEqual(normalizePath(URI.file('/foo/./bar')).toString(), 'file:///foo/bar'); + assert.strictEqual(normalizePath(URI.file('/foo/.')).toString(), 'file:///foo'); + assert.strictEqual(normalizePath(URI.file('/foo/./')).toString(), 'file:///foo/'); + assert.strictEqual(normalizePath(URI.file('/foo/..')).toString(), 'file:///'); + assert.strictEqual(normalizePath(URI.file('/foo/../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/./../../bar')).toString(), 'file:///bar'); + assert.strictEqual(normalizePath(URI.file('/foo/foo/./../some/../bar')).toString(), 'file:///foo/bar'); + assert.strictEqual(normalizePath(URI.file('/f')).toString(), 'file:///f'); } - assert.equal(normalizePath(URI.parse('foo://a/foo/./bar')).toString(), 'foo://a/foo/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/.')).toString(), 'foo://a/foo'); - assert.equal(normalizePath(URI.parse('foo://a/foo/./')).toString(), 'foo://a/foo/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/..')).toString(), 'foo://a/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../../bar')).toString(), 'foo://a/bar'); - assert.equal(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); - assert.equal(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); - assert.equal(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); - assert.equal(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString()); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/.')).toString(), 'foo://a/foo'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./')).toString(), 'foo://a/foo/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/..')).toString(), 'foo://a/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/./../../bar')).toString(), 'foo://a/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/foo/./../some/../bar')).toString(), 'foo://a/foo/bar'); + assert.strictEqual(normalizePath(URI.parse('foo://a')).toString(), 'foo://a'); + assert.strictEqual(normalizePath(URI.parse('foo://a/')).toString(), 'foo://a/'); + assert.strictEqual(normalizePath(URI.parse('foo://a/foo/./bar?q=1')).toString(), URI.parse('foo://a/foo/bar?q%3D1').toString()); }); test('isAbsolute', () => { if (isWindows) { - assert.equal(isAbsolutePath(URI.file('c:\\foo\\')), true); - assert.equal(isAbsolutePath(URI.file('C:\\foo\\')), true); - assert.equal(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute + assert.strictEqual(isAbsolutePath(URI.file('c:\\foo\\')), true); + assert.strictEqual(isAbsolutePath(URI.file('C:\\foo\\')), true); + assert.strictEqual(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute } else { - assert.equal(isAbsolutePath(URI.file('/foo/bar')), true); - assert.equal(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute + assert.strictEqual(isAbsolutePath(URI.file('/foo/bar')), true); + assert.strictEqual(isAbsolutePath(URI.file('bar')), true); // URI normalizes all file URIs to be absolute } - assert.equal(isAbsolutePath(URI.parse('foo:foo')), false); - assert.equal(isAbsolutePath(URI.parse('foo://a/foo/.')), true); + assert.strictEqual(isAbsolutePath(URI.parse('foo:foo')), false); + assert.strictEqual(isAbsolutePath(URI.parse('foo://a/foo/.')), true); }); function assertTrailingSeparator(u1: URI, expected: boolean) { - assert.equal(hasTrailingPathSeparator(u1), expected, u1.toString()); + assert.strictEqual(hasTrailingPathSeparator(u1), expected, u1.toString()); } function assertRemoveTrailingSeparator(u1: URI, expected: URI) { @@ -237,14 +237,14 @@ suite('Resources', () => { function assertEqualURI(actual: URI, expected: URI, message?: string, ignoreCase?: boolean) { let util = ignoreCase ? extUriIgnorePathCase : extUri; if (!util.isEqual(expected, actual)) { - assert.equal(actual.toString(), expected.toString(), message); + assert.strictEqual(actual.toString(), expected.toString(), message); } } function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean, ignoreCase?: boolean) { let util = ignoreCase ? extUriIgnorePathCase : extUri; - assert.equal(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); + assert.strictEqual(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); if (expectedPath !== undefined && !ignoreJoin) { assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal', ignoreCase); } @@ -304,7 +304,7 @@ suite('Resources', () => { if (!p.isAbsolute(path)) { let expectedPath = isWindows ? toSlashes(path) : path; expectedPath = expectedPath.startsWith('./') ? expectedPath.substr(2) : expectedPath; - assert.equal(relativePath(u1, actual), expectedPath, `relativePath (${u1.toString()}) on actual (${actual.toString()}) should be to path (${expectedPath})`); + assert.strictEqual(relativePath(u1, actual), expectedPath, `relativePath (${u1.toString()}) on actual (${actual.toString()}) should be to path (${expectedPath})`); } } @@ -352,12 +352,12 @@ suite('Resources', () => { let util = ignoreCase ? extUriIgnorePathCase : extUri; - assert.equal(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); - assert.equal(util.compare(u1, u2) === 0, expected); - assert.equal(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); - assert.equal(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); + assert.strictEqual(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); + assert.strictEqual(util.compare(u1, u2) === 0, expected); + assert.strictEqual(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); + assert.strictEqual(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); if (!ignoreCase) { - assert.equal(u1.toString() === u2.toString(), expected); + assert.strictEqual(u1.toString() === u2.toString(), expected); } } @@ -402,31 +402,31 @@ suite('Resources', () => { let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar'); let fileURI2 = isWindows ? URI.file('c:\\foo') : URI.file('/foo'); let fileURI2b = isWindows ? URI.file('C:\\Foo\\') : URI.file('/Foo/'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); - assert.equal(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); + assert.strictEqual(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); - assert.equal(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); + assert.strictEqual(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); let fileURI3 = URI.parse('foo://server:453/foo/bar/goo'); let fileURI4 = URI.parse('foo://server:453/foo/'); let fileURI5 = URI.parse('foo://server:453/foo'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); - assert.equal(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); - assert.equal(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); + assert.strictEqual(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); + assert.strictEqual(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); let fileURI6 = URI.parse('foo://server:453/foo?q=1'); let fileURI7 = URI.parse('foo://server:453/foo/bar?q=1'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); - assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); + assert.strictEqual(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); }); }); diff --git a/src/vs/base/test/common/scrollable.test.ts b/src/vs/base/test/common/scrollable.test.ts index b2cc21cc56..4607555375 100644 --- a/src/vs/base/test/common/scrollable.test.ts +++ b/src/vs/base/test/common/scrollable.test.ts @@ -57,7 +57,7 @@ suite('SmoothScrollingOperation', () => { function assertSmoothScroll(from: number, to: number, expected: [number, number][]): void { const actual = simulateSmoothScroll(from, to); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('scroll 25 lines (40 fit)', () => { diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts index 9e579095fd..338e842983 100644 --- a/src/vs/base/test/common/skipList.test.ts +++ b/src/vs/base/test/common/skipList.test.ts @@ -12,45 +12,45 @@ import { binarySearch } from 'vs/base/common/arrays'; suite('SkipList', function () { function assertValues(list: SkipList, expected: V[]) { - assert.equal(list.size, expected.length); - assert.deepEqual([...list.values()], expected); + assert.strictEqual(list.size, expected.length); + assert.deepStrictEqual([...list.values()], expected); let valuesFromEntries = [...list.entries()].map(entry => entry[1]); - assert.deepEqual(valuesFromEntries, expected); + assert.deepStrictEqual(valuesFromEntries, expected); let valuesFromIter = [...list].map(entry => entry[1]); - assert.deepEqual(valuesFromIter, expected); + assert.deepStrictEqual(valuesFromIter, expected); let i = 0; list.forEach((value, _key, map) => { assert.ok(map === list); - assert.deepEqual(value, expected[i++]); + assert.deepStrictEqual(value, expected[i++]); }); } function assertKeys(list: SkipList, expected: K[]) { - assert.equal(list.size, expected.length); - assert.deepEqual([...list.keys()], expected); + assert.strictEqual(list.size, expected.length); + assert.deepStrictEqual([...list.keys()], expected); let keysFromEntries = [...list.entries()].map(entry => entry[0]); - assert.deepEqual(keysFromEntries, expected); + assert.deepStrictEqual(keysFromEntries, expected); let keysFromIter = [...list].map(entry => entry[0]); - assert.deepEqual(keysFromIter, expected); + assert.deepStrictEqual(keysFromIter, expected); let i = 0; list.forEach((_value, key, map) => { assert.ok(map === list); - assert.deepEqual(key, expected[i++]); + assert.deepStrictEqual(key, expected[i++]); }); } test('set/get/delete', function () { let list = new SkipList((a, b) => a - b); - assert.equal(list.get(3), undefined); + assert.strictEqual(list.get(3), undefined); list.set(3, 1); - assert.equal(list.get(3), 1); + assert.strictEqual(list.get(3), 1); assertValues(list, [1]); list.set(3, 3); @@ -58,17 +58,17 @@ suite('SkipList', function () { list.set(1, 1); list.set(4, 4); - assert.equal(list.get(3), 3); - assert.equal(list.get(1), 1); - assert.equal(list.get(4), 4); + assert.strictEqual(list.get(3), 3); + assert.strictEqual(list.get(1), 1); + assert.strictEqual(list.get(4), 4); assertValues(list, [1, 3, 4]); - assert.equal(list.delete(17), false); + assert.strictEqual(list.delete(17), false); - assert.equal(list.delete(1), true); - assert.equal(list.get(1), undefined); - assert.equal(list.get(3), 3); - assert.equal(list.get(4), 4); + assert.strictEqual(list.delete(1), true); + assert.strictEqual(list.get(1), undefined); + assert.strictEqual(list.get(3), 3); + assert.strictEqual(list.get(4), 4); assertValues(list, [3, 4]); }); @@ -87,7 +87,7 @@ suite('SkipList', function () { assertKeys(list, [3, 6, 7, 9, 12, 19, 21, 25]); list.set(17, true); - assert.deepEqual(list.size, 9); + assert.deepStrictEqual(list.size, 9); assertKeys(list, [3, 6, 7, 9, 12, 17, 19, 21, 25]); }); @@ -141,8 +141,7 @@ suite('SkipList', function () { } - test('perf', function () { - this.skip(); + test.skip('perf', function () { // data const max = 2 ** 16; diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index bc1752b69e..c0ad343e8d 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream } from 'vs/base/common/stream'; +import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, observe } from 'vs/base/common/stream'; import { timeout } from 'vs/base/common/async'; suite('Stream', () => { @@ -43,37 +43,37 @@ suite('Stream', () => { chunks.push(data); }); - assert.equal(chunks[0], 'Hello'); + assert.strictEqual(chunks[0], 'Hello'); stream.write('World'); - assert.equal(chunks[1], 'World'); + assert.strictEqual(chunks[1], 'World'); - assert.equal(error, false); - assert.equal(end, false); + assert.strictEqual(error, false); + assert.strictEqual(end, false); stream.pause(); stream.write('1'); stream.write('2'); stream.write('3'); - assert.equal(chunks.length, 2); + assert.strictEqual(chunks.length, 2); stream.resume(); - assert.equal(chunks.length, 3); - assert.equal(chunks[2], '1,2,3'); + assert.strictEqual(chunks.length, 3); + assert.strictEqual(chunks[2], '1,2,3'); stream.error(new Error()); - assert.equal(error, true); + assert.strictEqual(error, true); stream.end('Final Bit'); - assert.equal(chunks.length, 4); - assert.equal(chunks[3], 'Final Bit'); + assert.strictEqual(chunks.length, 4); + assert.strictEqual(chunks[3], 'Final Bit'); stream.destroy(); stream.write('Unexpected'); - assert.equal(chunks.length, 4); + assert.strictEqual(chunks.length, 4); }); test('WriteableStream - removeListener', () => { @@ -92,22 +92,24 @@ suite('Stream', () => { stream.on('data', dataListener); stream.write('Hello'); - assert.equal(data, true); + assert.strictEqual(data, true); data = false; stream.removeListener('data', dataListener); stream.write('World'); - assert.equal(data, false); + assert.strictEqual(data, false); stream.error(new Error()); - assert.equal(error, true); + assert.strictEqual(error, true); error = false; stream.removeListener('error', errorListener); + // always leave at least one error listener to streams to avoid unexpected errors during test running + stream.on('error', () => { }); stream.error(new Error()); - assert.equal(error, false); + assert.strictEqual(error, false); }); test('WriteableStream - highWaterMark', async () => { @@ -147,14 +149,14 @@ suite('Stream', () => { assert.ok(data); await timeout(0); - assert.equal(drained1, true); - assert.equal(drained2, true); + assert.strictEqual(drained1, true); + assert.strictEqual(drained2, true); }); test('consumeReadable', () => { const readable = arrayToReadable(['1', '2', '3', '4', '5']); const consumed = consumeReadable(readable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('peekReadable', () => { @@ -166,17 +168,17 @@ suite('Stream', () => { assert.fail('Unexpected result'); } else { const consumed = consumeReadable(consumedOrReadable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); } } let readable = arrayToReadable(['1', '2', '3', '4', '5']); let consumedOrReadable = peekReadable(readable, strings => strings.join(), 5); - assert.equal(consumedOrReadable, '1,2,3,4,5'); + assert.strictEqual(consumedOrReadable, '1,2,3,4,5'); readable = arrayToReadable(['1', '2', '3', '4', '5']); consumedOrReadable = peekReadable(readable, strings => strings.join(), 6); - assert.equal(consumedOrReadable, '1,2,3,4,5'); + assert.strictEqual(consumedOrReadable, '1,2,3,4,5'); }); test('peekReadable - error handling', async () => { @@ -265,7 +267,7 @@ suite('Stream', () => { test('consumeStream', async () => { const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); const consumed = await consumeStream(stream, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('peekStream', async () => { @@ -273,11 +275,11 @@ suite('Stream', () => { const stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); const result = await peekStream(stream, i); - assert.equal(stream, result.stream); + assert.strictEqual(stream, result.stream); if (result.ended) { assert.fail('Unexpected result, stream should not have ended yet'); } else { - assert.equal(result.buffer.length, i + 1, `maxChunks: ${i}`); + assert.strictEqual(result.buffer.length, i + 1, `maxChunks: ${i}`); const additionalResult: string[] = []; await consumeStream(stream, strings => { @@ -286,33 +288,33 @@ suite('Stream', () => { return strings.join(); }); - assert.equal([...result.buffer, ...additionalResult].join(), '1,2,3,4,5'); + assert.strictEqual([...result.buffer, ...additionalResult].join(), '1,2,3,4,5'); } } let stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); let result = await peekStream(stream, 5); - assert.equal(stream, result.stream); - assert.equal(result.buffer.join(), '1,2,3,4,5'); - assert.equal(result.ended, true); + assert.strictEqual(stream, result.stream); + assert.strictEqual(result.buffer.join(), '1,2,3,4,5'); + assert.strictEqual(result.ended, true); stream = readableToStream(arrayToReadable(['1', '2', '3', '4', '5'])); result = await peekStream(stream, 6); - assert.equal(stream, result.stream); - assert.equal(result.buffer.join(), '1,2,3,4,5'); - assert.equal(result.ended, true); + assert.strictEqual(stream, result.stream); + assert.strictEqual(result.buffer.join(), '1,2,3,4,5'); + assert.strictEqual(result.ended, true); }); test('toStream', async () => { const stream = toStream('1,2,3,4,5', strings => strings.join()); const consumed = await consumeStream(stream, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('toReadable', async () => { const readable = toReadable('1,2,3,4,5'); const consumed = await consumeReadable(readable, strings => strings.join()); - assert.equal(consumed, '1,2,3,4,5'); + assert.strictEqual(consumed, '1,2,3,4,5'); }); test('transform', async () => { @@ -330,6 +332,47 @@ suite('Stream', () => { }, 0); const consumed = await consumeStream(result, strings => strings.join()); - assert.equal(consumed, '11,22,33,44,55'); + assert.strictEqual(consumed, '11,22,33,44,55'); + }); + + test('observer', async () => { + const source1 = newWriteableStream(strings => strings.join()); + setTimeout(() => source1.error(new Error())); + await observe(source1).errorOrEnd(); + + const source2 = newWriteableStream(strings => strings.join()); + setTimeout(() => source2.end('Hello Test')); + await observe(source2).errorOrEnd(); + + const source3 = newWriteableStream(strings => strings.join()); + setTimeout(() => { + source3.write('Hello Test'); + source3.error(new Error()); + }); + await observe(source3).errorOrEnd(); + + const source4 = newWriteableStream(strings => strings.join()); + setTimeout(() => { + source4.write('Hello Test'); + source4.end(); + }); + await observe(source4).errorOrEnd(); + }); + + test('events are delivered even if a listener is removed during delivery', () => { + const stream = newWriteableStream(strings => strings.join()); + + let listener1Called = false; + let listener2Called = false; + + const listener1 = () => { stream.removeListener('end', listener1); listener1Called = true; }; + const listener2 = () => { listener2Called = true; }; + stream.on('end', listener1); + stream.on('end', listener2); + stream.on('data', () => { }); + stream.end(''); + + assert.strictEqual(listener1Called, true); + assert.strictEqual(listener2Called, true); }); }); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index e2ae999e27..af169b4942 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -52,7 +52,7 @@ suite('Strings', () => { let expected = strings.compare(a.toLowerCase(), b.toLowerCase()); expected = expected > 0 ? 1 : expected < 0 ? -1 : expected; - assert.equal(actual, expected, `${a} <> ${b}`); + assert.strictEqual(actual, expected, `${a} <> ${b}`); if (recurse) { assertCompareIgnoreCase(b, a, false); @@ -89,7 +89,7 @@ suite('Strings', () => { let expected = strings.compare(a.toLowerCase().substring(aStart, aEnd), b.toLowerCase().substring(bStart, bEnd)); expected = expected > 0 ? 1 : expected < 0 ? -1 : expected; - assert.equal(actual, expected, `${a} <> ${b}`); + assert.strictEqual(actual, expected, `${a} <> ${b}`); if (recurse) { assertCompareIgnoreCase(b, a, bStart, bEnd, aStart, aEnd, false); @@ -188,36 +188,41 @@ suite('Strings', () => { }); test('containsRTL', () => { - assert.equal(strings.containsRTL('a'), false); - assert.equal(strings.containsRTL(''), false); - assert.equal(strings.containsRTL(strings.UTF8_BOM_CHARACTER + 'a'), false); - assert.equal(strings.containsRTL('hello world!'), false); - assert.equal(strings.containsRTL('a📚📚b'), false); - assert.equal(strings.containsRTL('هناك حقيقة مثبتة منذ زمن طويل'), true); - assert.equal(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true); + assert.strictEqual(strings.containsRTL('a'), false); + assert.strictEqual(strings.containsRTL(''), false); + assert.strictEqual(strings.containsRTL(strings.UTF8_BOM_CHARACTER + 'a'), false); + assert.strictEqual(strings.containsRTL('hello world!'), false); + assert.strictEqual(strings.containsRTL('a📚📚b'), false); + assert.strictEqual(strings.containsRTL('هناك حقيقة مثبتة منذ زمن طويل'), true); + assert.strictEqual(strings.containsRTL('זוהי עובדה מבוססת שדעתו'), true); }); test('containsEmoji', () => { - assert.equal(strings.containsEmoji('a'), false); - assert.equal(strings.containsEmoji(''), false); - assert.equal(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false); - assert.equal(strings.containsEmoji('hello world!'), false); - assert.equal(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false); - assert.equal(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false); + assert.strictEqual(strings.containsEmoji('a'), false); + assert.strictEqual(strings.containsEmoji(''), false); + assert.strictEqual(strings.containsEmoji(strings.UTF8_BOM_CHARACTER + 'a'), false); + assert.strictEqual(strings.containsEmoji('hello world!'), false); + assert.strictEqual(strings.containsEmoji('هناك حقيقة مثبتة منذ زمن طويل'), false); + assert.strictEqual(strings.containsEmoji('זוהי עובדה מבוססת שדעתו'), false); - assert.equal(strings.containsEmoji('a📚📚b'), true); - assert.equal(strings.containsEmoji('1F600 # 😀 grinning face'), true); - assert.equal(strings.containsEmoji('1F47E # 👾 alien monster'), true); - assert.equal(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true); - assert.equal(strings.containsEmoji('26EA # ⛪ church'), true); - assert.equal(strings.containsEmoji('231B # ⌛ hourglass'), true); - assert.equal(strings.containsEmoji('2702 # ✂ scissors'), true); - assert.equal(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true); + assert.strictEqual(strings.containsEmoji('a📚📚b'), true); + assert.strictEqual(strings.containsEmoji('1F600 # 😀 grinning face'), true); + assert.strictEqual(strings.containsEmoji('1F47E # 👾 alien monster'), true); + assert.strictEqual(strings.containsEmoji('1F467 1F3FD # 👧🏽 girl: medium skin tone'), true); + assert.strictEqual(strings.containsEmoji('26EA # ⛪ church'), true); + assert.strictEqual(strings.containsEmoji('231B # ⌛ hourglass'), true); + assert.strictEqual(strings.containsEmoji('2702 # ✂ scissors'), true); + assert.strictEqual(strings.containsEmoji('1F1F7 1F1F4 # 🇷🇴 Romania'), true); + }); + + test('issue #115221: isEmojiImprecise misses ⭐', () => { + const codePoint = strings.getNextCodePoint('⭐', '⭐'.length, 0); + assert.strictEqual(strings.isEmojiImprecise(codePoint), true); }); test('isBasicASCII', () => { function assertIsBasicASCII(str: string, expected: boolean): void { - assert.equal(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); + assert.strictEqual(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); } assertIsBasicASCII('abcdefghijklmnopqrstuvwxyz', true); assertIsBasicASCII('ABCDEFGHIJKLMNOPQRSTUVWXYZ', true); @@ -245,16 +250,16 @@ suite('Strings', () => { assert.throws(() => strings.createRegExp('', false)); // Escapes appropriately - assert.equal(strings.createRegExp('abc', false).source, 'abc'); - assert.equal(strings.createRegExp('([^ ,.]*)', false).source, '\\(\\[\\^ ,\\.\\]\\*\\)'); - assert.equal(strings.createRegExp('([^ ,.]*)', true).source, '([^ ,.]*)'); + assert.strictEqual(strings.createRegExp('abc', false).source, 'abc'); + assert.strictEqual(strings.createRegExp('([^ ,.]*)', false).source, '\\(\\[\\^ ,\\.\\]\\*\\)'); + assert.strictEqual(strings.createRegExp('([^ ,.]*)', true).source, '([^ ,.]*)'); // Whole word - assert.equal(strings.createRegExp('abc', false, { wholeWord: true }).source, '\\babc\\b'); - assert.equal(strings.createRegExp('abc', true, { wholeWord: true }).source, '\\babc\\b'); - assert.equal(strings.createRegExp(' abc', true, { wholeWord: true }).source, ' abc\\b'); - assert.equal(strings.createRegExp('abc ', true, { wholeWord: true }).source, '\\babc '); - assert.equal(strings.createRegExp(' abc ', true, { wholeWord: true }).source, ' abc '); + assert.strictEqual(strings.createRegExp('abc', false, { wholeWord: true }).source, '\\babc\\b'); + assert.strictEqual(strings.createRegExp('abc', true, { wholeWord: true }).source, '\\babc\\b'); + assert.strictEqual(strings.createRegExp(' abc', true, { wholeWord: true }).source, ' abc\\b'); + assert.strictEqual(strings.createRegExp('abc ', true, { wholeWord: true }).source, '\\babc '); + assert.strictEqual(strings.createRegExp(' abc ', true, { wholeWord: true }).source, ' abc '); const regExpWithoutFlags = strings.createRegExp('abc', true); assert(!regExpWithoutFlags.global); @@ -284,15 +289,15 @@ suite('Strings', () => { }); test('getLeadingWhitespace', () => { - assert.equal(strings.getLeadingWhitespace(' foo'), ' '); - assert.equal(strings.getLeadingWhitespace(' foo', 2), ''); - assert.equal(strings.getLeadingWhitespace(' foo', 1, 1), ''); - assert.equal(strings.getLeadingWhitespace(' foo', 0, 1), ' '); - assert.equal(strings.getLeadingWhitespace(' '), ' '); - assert.equal(strings.getLeadingWhitespace(' ', 1), ' '); - assert.equal(strings.getLeadingWhitespace(' ', 0, 1), ' '); - assert.equal(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 1), '\t'); - assert.equal(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 2), '\t\t'); + assert.strictEqual(strings.getLeadingWhitespace(' foo'), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 2), ''); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 1, 1), ''); + assert.strictEqual(strings.getLeadingWhitespace(' foo', 0, 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' '), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' ', 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace(' ', 0, 1), ' '); + assert.strictEqual(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 1), '\t'); + assert.strictEqual(strings.getLeadingWhitespace('\t\tfunction foo(){', 0, 2), '\t\t'); }); test('fuzzyContains', () => { @@ -316,11 +321,11 @@ suite('Strings', () => { }); test('stripUTF8BOM', () => { - assert.equal(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER), ''); - assert.equal(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER + 'foobar'), 'foobar'); - assert.equal(strings.stripUTF8BOM('foobar' + strings.UTF8_BOM_CHARACTER), 'foobar' + strings.UTF8_BOM_CHARACTER); - assert.equal(strings.stripUTF8BOM('abc'), 'abc'); - assert.equal(strings.stripUTF8BOM(''), ''); + assert.strictEqual(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER), ''); + assert.strictEqual(strings.stripUTF8BOM(strings.UTF8_BOM_CHARACTER + 'foobar'), 'foobar'); + assert.strictEqual(strings.stripUTF8BOM('foobar' + strings.UTF8_BOM_CHARACTER), 'foobar' + strings.UTF8_BOM_CHARACTER); + assert.strictEqual(strings.stripUTF8BOM('abc'), 'abc'); + assert.strictEqual(strings.stripUTF8BOM(''), ''); }); test('containsUppercaseCharacter', () => { @@ -340,7 +345,7 @@ suite('Strings', () => { ['FöÖ', true], ['\\Foo', true], ].forEach(([str, result]) => { - assert.equal(strings.containsUppercaseCharacter(str), result, `Wrong result for ${str}`); + assert.strictEqual(strings.containsUppercaseCharacter(str), result, `Wrong result for ${str}`); }); }); @@ -352,7 +357,7 @@ suite('Strings', () => { ['Foo', true], ].forEach(([str, result]) => { - assert.equal(strings.containsUppercaseCharacter(str, true), result, `Wrong result for ${str}`); + assert.strictEqual(strings.containsUppercaseCharacter(str, true), result, `Wrong result for ${str}`); }); }); @@ -364,20 +369,20 @@ suite('Strings', () => { ['123', '123'], ['.a', '.a'], ].forEach(([inStr, result]) => { - assert.equal(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`); + assert.strictEqual(strings.uppercaseFirstLetter(inStr), result, `Wrong result for ${inStr}`); }); }); test('getNLines', () => { - assert.equal(strings.getNLines('', 5), ''); - assert.equal(strings.getNLines('foo', 5), 'foo'); - assert.equal(strings.getNLines('foo\nbar', 5), 'foo\nbar'); - assert.equal(strings.getNLines('foo\nbar', 2), 'foo\nbar'); + assert.strictEqual(strings.getNLines('', 5), ''); + assert.strictEqual(strings.getNLines('foo', 5), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar', 5), 'foo\nbar'); + assert.strictEqual(strings.getNLines('foo\nbar', 2), 'foo\nbar'); - assert.equal(strings.getNLines('foo\nbar', 1), 'foo'); - assert.equal(strings.getNLines('foo\nbar'), 'foo'); - assert.equal(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar'); - assert.equal(strings.getNLines('foo', 0), ''); + assert.strictEqual(strings.getNLines('foo\nbar', 1), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar'), 'foo'); + assert.strictEqual(strings.getNLines('foo\nbar\nsomething', 2), 'foo\nbar'); + assert.strictEqual(strings.getNLines('foo', 0), ''); }); test('encodeUTF8', function () { @@ -387,12 +392,12 @@ suite('Strings', () => { for (let offset = 0; offset < actual.byteLength; offset++) { actualArr[offset] = actual[offset]; } - assert.deepEqual(actualArr, expected); + assert.deepStrictEqual(actualArr, expected); } function assertDecodeUTF8(data: number[], expected: string): void { const actual = strings.decodeUTF8(new Uint8Array(data)); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } function assertEncodeDecodeUTF8(str: string, buff: number[]): void { @@ -415,11 +420,11 @@ suite('Strings', () => { }); test('getGraphemeBreakType', () => { - assert.equal(strings.getGraphemeBreakType(0xBC1), strings.GraphemeBreakType.SpacingMark); + assert.strictEqual(strings.getGraphemeBreakType(0xBC1), strings.GraphemeBreakType.SpacingMark); }); test('truncate', () => { - assert.equal('hello world', strings.truncate('hello world', 100)); - assert.equal('hello…', strings.truncate('hello world', 5)); + assert.strictEqual('hello world', strings.truncate('hello world', 100)); + assert.strictEqual('hello…', strings.truncate('hello world', 5)); }); }); diff --git a/src/vs/base/test/common/troubleshooting.ts b/src/vs/base/test/common/troubleshooting.ts new file mode 100644 index 0000000000..c7ba292780 --- /dev/null +++ b/src/vs/base/test/common/troubleshooting.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, IDisposableTracker, setDisposableTracker } from 'vs/base/common/lifecycle'; + +class DisposableTracker implements IDisposableTracker { + allDisposables: [IDisposable, string][] = []; + trackDisposable(x: IDisposable): void { + this.allDisposables.push([x, new Error().stack!]); + } + markTracked(x: IDisposable): void { + for (let idx = 0; idx < this.allDisposables.length; idx++) { + if (this.allDisposables[idx][0] === x) { + this.allDisposables.splice(idx, 1); + return; + } + } + } +} + +let currentTracker: DisposableTracker | null = null; + +export function beginTrackingDisposables(): void { + currentTracker = new DisposableTracker(); + setDisposableTracker(currentTracker); +} + +export function endTrackingDisposables(): void { + if (currentTracker) { + setDisposableTracker(null); + console.log(currentTracker!.allDisposables.map(e => `${e[0]}\n${e[1]}`).join('\n\n')); + currentTracker = null; + } +} + +export function beginLoggingFS(withStacks: boolean = false): void { + if ((self).beginLoggingFS) { + (self).beginLoggingFS(withStacks); + } +} + +export function endLoggingFS(): void { + if ((self).endLoggingFS) { + (self).endLoggingFS(); + } +} diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index d4817bc78f..8eb0ae1a76 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -57,7 +57,7 @@ suite('Types', () => { assert(!types.isObject(/test/)); assert(!types.isObject(new RegExp(''))); assert(!types.isFunction(new Date())); - assert(!types.isObject(assert)); + assert.strictEqual(types.isObject(assert), false); assert(!types.isObject(function foo() { })); assert(types.isObject({})); @@ -75,7 +75,7 @@ suite('Types', () => { assert(!types.isEmptyObject(/test/)); assert(!types.isEmptyObject(new RegExp(''))); assert(!types.isEmptyObject(new Date())); - assert(!types.isEmptyObject(assert)); + assert.strictEqual(types.isEmptyObject(assert), false); assert(!types.isEmptyObject(function foo() { /**/ })); assert(!types.isEmptyObject({ foo: 'bar' })); @@ -178,15 +178,15 @@ suite('Types', () => { assert.throws(() => types.assertAllDefined(true, undefined)); assert.throws(() => types.assertAllDefined(undefined, false)); - assert.equal(types.assertIsDefined(true), true); - assert.equal(types.assertIsDefined(false), false); - assert.equal(types.assertIsDefined('Hello'), 'Hello'); - assert.equal(types.assertIsDefined(''), ''); + assert.strictEqual(types.assertIsDefined(true), true); + assert.strictEqual(types.assertIsDefined(false), false); + assert.strictEqual(types.assertIsDefined('Hello'), 'Hello'); + assert.strictEqual(types.assertIsDefined(''), ''); const res = types.assertAllDefined(1, true, 'Hello'); - assert.equal(res[0], 1); - assert.equal(res[1], true); - assert.equal(res[2], 'Hello'); + assert.strictEqual(res[0], 1); + assert.strictEqual(res[1], true); + assert.strictEqual(res[2], 'Hello'); }); test('validateConstraints', () => { diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index f15093a58d..a81fc95ed4 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -9,82 +9,82 @@ import { isWindows } from 'vs/base/common/platform'; suite('URI', () => { test('file#toString', () => { - assert.equal(URI.file('c:/win/path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('C:/win/path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('c:/win/path/').toString(), 'file:///c%3A/win/path/'); - assert.equal(URI.file('/c:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('C:/win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:/win/path/').toString(), 'file:///c%3A/win/path/'); + assert.strictEqual(URI.file('/c:/win/path').toString(), 'file:///c%3A/win/path'); }); test('URI.file (win-special)', () => { if (isWindows) { - assert.equal(URI.file('c:\\win\\path').toString(), 'file:///c%3A/win/path'); - assert.equal(URI.file('c:\\win/path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:\\win\\path').toString(), 'file:///c%3A/win/path'); + assert.strictEqual(URI.file('c:\\win/path').toString(), 'file:///c%3A/win/path'); } else { - assert.equal(URI.file('c:\\win\\path').toString(), 'file:///c%3A%5Cwin%5Cpath'); - assert.equal(URI.file('c:\\win/path').toString(), 'file:///c%3A%5Cwin/path'); + assert.strictEqual(URI.file('c:\\win\\path').toString(), 'file:///c%3A%5Cwin%5Cpath'); + assert.strictEqual(URI.file('c:\\win/path').toString(), 'file:///c%3A%5Cwin/path'); } }); test('file#fsPath (win-special)', () => { if (isWindows) { - assert.equal(URI.file('c:\\win\\path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:\\win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:\\win\\path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:\\win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); - assert.equal(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); - assert.equal(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); + assert.strictEqual(URI.file('c:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('c:/win/path/').fsPath, 'c:\\win\\path\\'); + assert.strictEqual(URI.file('C:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('/c:/win/path').fsPath, 'c:\\win\\path'); + assert.strictEqual(URI.file('./c/win/path').fsPath, '\\.\\c\\win\\path'); } else { - assert.equal(URI.file('c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); - assert.equal(URI.file('C:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('/c:/win/path').fsPath, 'c:/win/path'); - assert.equal(URI.file('./c/win/path').fsPath, '/./c/win/path'); + assert.strictEqual(URI.file('c:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('c:/win/path/').fsPath, 'c:/win/path/'); + assert.strictEqual(URI.file('C:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('/c:/win/path').fsPath, 'c:/win/path'); + assert.strictEqual(URI.file('./c/win/path').fsPath, '/./c/win/path'); } }); test('URI#fsPath - no `fsPath` when no `path`', () => { const value = URI.parse('file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp'); - assert.equal(value.authority, '/home/ticino/desktop/cpluscplus/test.cpp'); - assert.equal(value.path, '/'); + assert.strictEqual(value.authority, '/home/ticino/desktop/cpluscplus/test.cpp'); + assert.strictEqual(value.path, '/'); if (isWindows) { - assert.equal(value.fsPath, '\\'); + assert.strictEqual(value.fsPath, '\\'); } else { - assert.equal(value.fsPath, '/'); + assert.strictEqual(value.fsPath, '/'); } }); test('http#toString', () => { - assert.equal(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: 'www.MSFT.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); - assert.equal(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.msft.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'www.MSFT.com', path: '/my/path' }).toString(), 'http://www.msft.com/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); + assert.strictEqual(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); //http://a-test-site.com/#test=true - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); }); test('http#toString, encode=FALSE', () => { - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(true), 'http://a-test-site.com/?test=true'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(true), 'http://a-test-site.com/#test=true'); - assert.equal(URI.from({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(true), 'http:/api/files/test.me?t=1234'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(true), 'http://a-test-site.com/?test=true'); + assert.strictEqual(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(true), 'http://a-test-site.com/#test=true'); + assert.strictEqual(URI.from({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(true), 'http:/api/files/test.me?t=1234'); const value = URI.parse('file://shares/pröjects/c%23/#l12'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/pröjects/c#/'); - assert.equal(value.fragment, 'l12'); - assert.equal(value.toString(), 'file://shares/pr%C3%B6jects/c%23/#l12'); - assert.equal(value.toString(true), 'file://shares/pröjects/c%23/#l12'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/pröjects/c#/'); + assert.strictEqual(value.fragment, 'l12'); + assert.strictEqual(value.toString(), 'file://shares/pr%C3%B6jects/c%23/#l12'); + assert.strictEqual(value.toString(true), 'file://shares/pröjects/c%23/#l12'); const uri2 = URI.parse(value.toString(true)); const uri3 = URI.parse(value.toString()); - assert.equal(uri2.authority, uri3.authority); - assert.equal(uri2.path, uri3.path); - assert.equal(uri2.query, uri3.query); - assert.equal(uri2.fragment, uri3.fragment); + assert.strictEqual(uri2.authority, uri3.authority); + assert.strictEqual(uri2.path, uri3.path); + assert.strictEqual(uri2.query, uri3.query); + assert.strictEqual(uri2.fragment, uri3.fragment); }); test('with, identity', () => { @@ -101,23 +101,23 @@ suite('URI', () => { }); test('with, changes', () => { - assert.equal(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); + assert.strictEqual(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); }); test('with, remove components #8465', () => { - assert.equal(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: '' }).toString(), 'scheme://authority'); - assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: null }).toString(), 'scheme://authority'); - assert.equal(URI.parse('scheme:/path').with({ authority: '' }).toString(), 'scheme:/path'); - assert.equal(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: '' }).toString(), 'scheme://authority'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ path: null }).toString(), 'scheme://authority'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: '' }).toString(), 'scheme:/path'); + assert.strictEqual(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); }); test('with, validation', () => { @@ -130,104 +130,104 @@ suite('URI', () => { test('parse', () => { let value = URI.parse('http:/api/files/test.me?t=1234'); - assert.equal(value.scheme, 'http'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/api/files/test.me'); - assert.equal(value.query, 't=1234'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'http'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/api/files/test.me'); + assert.strictEqual(value.query, 't=1234'); + assert.strictEqual(value.fragment, ''); value = URI.parse('http://api/files/test.me?t=1234'); - assert.equal(value.scheme, 'http'); - assert.equal(value.authority, 'api'); - assert.equal(value.path, '/files/test.me'); - assert.equal(value.query, 't=1234'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'http'); + assert.strictEqual(value.authority, 'api'); + assert.strictEqual(value.path, '/files/test.me'); + assert.strictEqual(value.query, 't=1234'); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:///c:/test/me'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/test/me'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.fsPath, isWindows ? 'c:\\test\\me' : 'c:/test/me'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/test/me'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fsPath, isWindows ? 'c:\\test\\me' : 'c:/test/me'); value = URI.parse('file://shares/files/c%23/p.cs'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/files/c#/p.cs'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.fsPath, isWindows ? '\\\\shares\\files\\c#\\p.cs' : '//shares/files/c#/p.cs'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/files/c#/p.cs'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fsPath, isWindows ? '\\\\shares\\files\\c#\\p.cs' : '//shares/files/c#/p.cs'); value = URI.parse('file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); value = URI.parse('file:///c:/test %25/path'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/c:/test %/path'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/c:/test %/path'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); value = URI.parse('inmemory:'); - assert.equal(value.scheme, 'inmemory'); - assert.equal(value.authority, ''); - assert.equal(value.path, ''); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'inmemory'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo:api/files/test'); - assert.equal(value.scheme, 'foo'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'api/files/test'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'api/files/test'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:?q'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/'); - assert.equal(value.query, 'q'); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/'); + assert.strictEqual(value.query, 'q'); + assert.strictEqual(value.fragment, ''); value = URI.parse('file:#d'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/'); - assert.equal(value.query, ''); - assert.equal(value.fragment, 'd'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, 'd'); value = URI.parse('f3ile:#d'); - assert.equal(value.scheme, 'f3ile'); - assert.equal(value.authority, ''); - assert.equal(value.path, ''); - assert.equal(value.query, ''); - assert.equal(value.fragment, 'd'); + assert.strictEqual(value.scheme, 'f3ile'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, 'd'); value = URI.parse('foo+bar:path'); - assert.equal(value.scheme, 'foo+bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo+bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo-bar:path'); - assert.equal(value.scheme, 'foo-bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo-bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); value = URI.parse('foo.bar:path'); - assert.equal(value.scheme, 'foo.bar'); - assert.equal(value.authority, ''); - assert.equal(value.path, 'path'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); + assert.strictEqual(value.scheme, 'foo.bar'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, 'path'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); }); test('parse, disallow //path when no authority', () => { @@ -237,132 +237,132 @@ suite('URI', () => { test('URI#file, win-speciale', () => { if (isWindows) { let value = URI.file('c:\\test\\drive'); - assert.equal(value.path, '/c:/test/drive'); - assert.equal(value.toString(), 'file:///c%3A/test/drive'); + assert.strictEqual(value.path, '/c:/test/drive'); + assert.strictEqual(value.toString(), 'file:///c%3A/test/drive'); value = URI.file('\\\\shäres\\path\\c#\\plugin.json'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shäres'); - assert.equal(value.path, '/path/c#/plugin.json'); - assert.equal(value.fragment, ''); - assert.equal(value.query, ''); - assert.equal(value.toString(), 'file://sh%C3%A4res/path/c%23/plugin.json'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shäres'); + assert.strictEqual(value.path, '/path/c#/plugin.json'); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.toString(), 'file://sh%C3%A4res/path/c%23/plugin.json'); value = URI.file('\\\\localhost\\c$\\GitDevelopment\\express'); - assert.equal(value.scheme, 'file'); - assert.equal(value.path, '/c$/GitDevelopment/express'); - assert.equal(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); - assert.equal(value.query, ''); - assert.equal(value.fragment, ''); - assert.equal(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.path, '/c$/GitDevelopment/express'); + assert.strictEqual(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); + assert.strictEqual(value.query, ''); + assert.strictEqual(value.fragment, ''); + assert.strictEqual(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); value = URI.file('c:\\test with %\\path'); - assert.equal(value.path, '/c:/test with %/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%25/path'); + assert.strictEqual(value.path, '/c:/test with %/path'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%25/path'); value = URI.file('c:\\test with %25\\path'); - assert.equal(value.path, '/c:/test with %25/path'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); + assert.strictEqual(value.path, '/c:/test with %25/path'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%2525/path'); value = URI.file('c:\\test with %25\\c#code'); - assert.equal(value.path, '/c:/test with %25/c#code'); - assert.equal(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); + assert.strictEqual(value.path, '/c:/test with %25/c#code'); + assert.strictEqual(value.toString(), 'file:///c%3A/test%20with%20%2525/c%23code'); value = URI.file('\\\\shares'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/'); // slash is always there + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/'); // slash is always there value = URI.file('\\\\shares\\'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, 'shares'); - assert.equal(value.path, '/'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, 'shares'); + assert.strictEqual(value.path, '/'); } }); test('VSCode URI module\'s driveLetterPath regex is incorrect, #32961', function () { let uri = URI.parse('file:///_:/path'); - assert.equal(uri.fsPath, isWindows ? '\\_:\\path' : '/_:/path'); + assert.strictEqual(uri.fsPath, isWindows ? '\\_:\\path' : '/_:/path'); }); test('URI#file, no path-is-uri check', () => { // we don't complain here let value = URI.file('file://path/to/file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/file://path/to/file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/file://path/to/file'); }); test('URI#file, always slash', () => { let value = URI.file('a.file'); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/a.file'); + assert.strictEqual(value.toString(), 'file:///a.file'); value = URI.parse(value.toString()); - assert.equal(value.scheme, 'file'); - assert.equal(value.authority, ''); - assert.equal(value.path, '/a.file'); - assert.equal(value.toString(), 'file:///a.file'); + assert.strictEqual(value.scheme, 'file'); + assert.strictEqual(value.authority, ''); + assert.strictEqual(value.path, '/a.file'); + assert.strictEqual(value.toString(), 'file:///a.file'); }); test('URI.toString, only scheme and query', () => { const value = URI.parse('stuff:?qüery'); - assert.equal(value.toString(), 'stuff:?q%C3%BCery'); + assert.strictEqual(value.toString(), 'stuff:?q%C3%BCery'); }); test('URI#toString, upper-case percent espaces', () => { const value = URI.parse('file://sh%c3%a4res/path'); - assert.equal(value.toString(), 'file://sh%C3%A4res/path'); + assert.strictEqual(value.toString(), 'file://sh%C3%A4res/path'); }); test('URI#toString, lower-case windows drive letter', () => { - assert.equal(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); - assert.equal(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.strictEqual(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.strictEqual(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); }); test('URI#toString, escape all the bits', () => { const value = URI.file('/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js'); - assert.equal(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); + assert.strictEqual(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); }); test('URI#toString, don\'t encode port', () => { let value = URI.parse('http://localhost:8080/far'); - assert.equal(value.toString(), 'http://localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assert.equal(value.toString(), 'http://l%C3%B6calhost:8080/far'); + assert.strictEqual(value.toString(), 'http://l%C3%B6calhost:8080/far'); }); test('URI#toString, user information in authority', () => { let value = URI.parse('http://foo:bar@localhost/far'); - assert.equal(value.toString(), 'http://foo:bar@localhost/far'); + assert.strictEqual(value.toString(), 'http://foo:bar@localhost/far'); value = URI.parse('http://foo@localhost/far'); - assert.equal(value.toString(), 'http://foo@localhost/far'); + assert.strictEqual(value.toString(), 'http://foo@localhost/far'); value = URI.parse('http://foo:bAr@localhost:8080/far'); - assert.equal(value.toString(), 'http://foo:bAr@localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://foo:bAr@localhost:8080/far'); value = URI.parse('http://foo@localhost:8080/far'); - assert.equal(value.toString(), 'http://foo@localhost:8080/far'); + assert.strictEqual(value.toString(), 'http://foo@localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assert.equal(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); + assert.strictEqual(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); }); test('correctFileUriToFilePath2', () => { const test = (input: string, expected: string) => { const value = URI.parse(input); - assert.equal(value.fsPath, expected, 'Result for ' + input); + assert.strictEqual(value.fsPath, expected, 'Result for ' + input); const value2 = URI.file(value.fsPath); - assert.equal(value2.fsPath, expected, 'Result for ' + input); - assert.equal(value.toString(), value2.toString()); + assert.strictEqual(value2.fsPath, expected, 'Result for ' + input); + assert.strictEqual(value.toString(), value2.toString()); }; test('file:///c:/alex.txt', isWindows ? 'c:\\alex.txt' : 'c:/alex.txt'); @@ -374,104 +374,132 @@ suite('URI', () => { test('URI - http, query & toString', function () { let uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.query, 'LinkId=518008'); - assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); + assert.strictEqual(uri.query, 'LinkId=518008'); + assert.strictEqual(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); + assert.strictEqual(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); let uri2 = URI.parse(uri.toString()); - assert.equal(uri2.query, 'LinkId=518008'); - assert.equal(uri2.query, uri.query); + assert.strictEqual(uri2.query, 'LinkId=518008'); + assert.strictEqual(uri2.query, uri.query); uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.query, 'LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); + assert.strictEqual(uri.query, 'LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); uri2 = URI.parse(uri.toString()); - assert.equal(uri2.query, 'LinkId=518008&foö&ké¥=üü'); - assert.equal(uri2.query, uri.query); + assert.strictEqual(uri2.query, 'LinkId=518008&foö&ké¥=üü'); + assert.strictEqual(uri2.query, uri.query); // #24849 uri = URI.parse('https://twitter.com/search?src=typd&q=%23tag'); - assert.equal(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); + assert.strictEqual(uri.toString(true), 'https://twitter.com/search?src=typd&q=%23tag'); }); test('class URI cannot represent relative file paths #34449', function () { let path = '/foo/bar'; - assert.equal(URI.file(path).path, path); + assert.strictEqual(URI.file(path).path, path); path = 'foo/bar'; - assert.equal(URI.file(path).path, '/foo/bar'); + assert.strictEqual(URI.file(path).path, '/foo/bar'); path = './foo/bar'; - assert.equal(URI.file(path).path, '/./foo/bar'); // missing normalization + assert.strictEqual(URI.file(path).path, '/./foo/bar'); // missing normalization const fileUri1 = URI.parse(`file:foo/bar`); - assert.equal(fileUri1.path, '/foo/bar'); - assert.equal(fileUri1.authority, ''); + assert.strictEqual(fileUri1.path, '/foo/bar'); + assert.strictEqual(fileUri1.authority, ''); const uri = fileUri1.toString(); - assert.equal(uri, 'file:///foo/bar'); + assert.strictEqual(uri, 'file:///foo/bar'); const fileUri2 = URI.parse(uri); - assert.equal(fileUri2.path, '/foo/bar'); - assert.equal(fileUri2.authority, ''); + assert.strictEqual(fileUri2.path, '/foo/bar'); + assert.strictEqual(fileUri2.authority, ''); }); test('Ctrl click to follow hash query param url gets urlencoded #49628', function () { let input = 'http://localhost:3000/#/foo?bar=baz'; let uri = URI.parse(input); - assert.equal(uri.toString(true), input); + assert.strictEqual(uri.toString(true), input); input = 'http://localhost:3000/foo?bar=baz'; uri = URI.parse(input); - assert.equal(uri.toString(true), input); + assert.strictEqual(uri.toString(true), input); }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { let uri = URI.file('/foo/%A0.txt'); let uri2 = URI.parse(uri.toString()); - assert.equal(uri.scheme, uri2.scheme); - assert.equal(uri.path, uri2.path); + assert.strictEqual(uri.scheme, uri2.scheme); + assert.strictEqual(uri.path, uri2.path); uri = URI.file('/foo/%2e.txt'); uri2 = URI.parse(uri.toString()); - assert.equal(uri.scheme, uri2.scheme); - assert.equal(uri.path, uri2.path); + assert.strictEqual(uri.scheme, uri2.scheme); + assert.strictEqual(uri.path, uri2.path); + }); + + test('Bug in URI.isUri() that fails `thing` type comparison #114971', function () { + const uri = URI.file('/foo/bazz.txt'); + assert.strictEqual(URI.isUri(uri), true); + assert.strictEqual(URI.isUri(uri.toJSON()), false); + + // fsPath -> getter + assert.strictEqual(URI.isUri({ + scheme: 'file', + authority: '', + path: '/foo/bazz.txt', + get fsPath() { return '/foo/bazz.txt'; }, + query: '', + fragment: '', + with() { return this; }, + toString() { return ''; } + }), true); + + // fsPath -> property + assert.strictEqual(URI.isUri({ + scheme: 'file', + authority: '', + path: '/foo/bazz.txt', + fsPath: '/foo/bazz.txt', + query: '', + fragment: '', + with() { return this; }, + toString() { return ''; } + }), true); }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { - assert.equal(URI.parse('file://some/%.txt'), 'file://some/%25.txt'); - assert.equal(URI.parse('file://some/%A0.txt'), 'file://some/%25A0.txt'); + assert.strictEqual(URI.parse('file://some/%.txt').toString(), 'file://some/%25.txt'); + assert.strictEqual(URI.parse('file://some/%A0.txt').toString(), 'file://some/%25A0.txt'); }); - test('Links in markdown are broken if url contains encoded parameters #79474', function () { - this.skip(); + test.skip('Links in markdown are broken if url contains encoded parameters #79474', function () { let strIn = 'https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'; let uri1 = URI.parse(strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assert.equal(uri1.scheme, uri2.scheme); - assert.equal(uri1.authority, uri2.authority); - assert.equal(uri1.path, uri2.path); - assert.equal(uri1.query, uri2.query); - assert.equal(uri1.fragment, uri2.fragment); - assert.equal(strIn, strOut); // fails here!! + assert.strictEqual(uri1.scheme, uri2.scheme); + assert.strictEqual(uri1.authority, uri2.authority); + assert.strictEqual(uri1.path, uri2.path); + assert.strictEqual(uri1.query, uri2.query); + assert.strictEqual(uri1.fragment, uri2.fragment); + assert.strictEqual(strIn, strOut); // fails here!! }); - test('Uri#parse can break path-component #45515', function () { - this.skip(); + test.skip('Uri#parse can break path-component #45515', function () { let strIn = 'https://firebasestorage.googleapis.com/v0/b/brewlangerie.appspot.com/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg?alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437'; let uri1 = URI.parse(strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assert.equal(uri1.scheme, uri2.scheme); - assert.equal(uri1.authority, uri2.authority); - assert.equal(uri1.path, uri2.path); - assert.equal(uri1.query, uri2.query); - assert.equal(uri1.fragment, uri2.fragment); - assert.equal(strIn, strOut); // fails here!! + assert.strictEqual(uri1.scheme, uri2.scheme); + assert.strictEqual(uri1.authority, uri2.authority); + assert.strictEqual(uri1.path, uri2.path); + assert.strictEqual(uri1.query, uri2.query); + assert.strictEqual(uri1.fragment, uri2.fragment); + assert.strictEqual(strIn, strOut); // fails here!! }); test('URI - (de)serialize', function () { @@ -492,13 +520,13 @@ suite('URI', () => { let data = value.toJSON() as UriComponents; let clone = URI.revive(data); - assert.equal(clone.scheme, value.scheme); - assert.equal(clone.authority, value.authority); - assert.equal(clone.path, value.path); - assert.equal(clone.query, value.query); - assert.equal(clone.fragment, value.fragment); - assert.equal(clone.fsPath, value.fsPath); - assert.equal(clone.toString(), value.toString()); + assert.strictEqual(clone.scheme, value.scheme); + assert.strictEqual(clone.authority, value.authority); + assert.strictEqual(clone.path, value.path); + assert.strictEqual(clone.query, value.query); + assert.strictEqual(clone.fragment, value.fragment); + assert.strictEqual(clone.fsPath, value.fsPath); + assert.strictEqual(clone.toString(), value.toString()); } // } // console.profileEnd(); @@ -507,11 +535,11 @@ suite('URI', () => { const baseUri = URI.parse(base); const newUri = URI.joinPath(baseUri, fragment); const actual = newUri.toString(true); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); if (checkWithUrl) { const actualUrl = new URL(fragment, base).href; - assert.equal(actualUrl, expected, 'DIFFERENT from URL'); + assert.strictEqual(actualUrl, expected, 'DIFFERENT from URL'); } } test('URI#joinPath', function () { diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index 1c25ddea1e..cb33037930 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -5,47 +5,10 @@ import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { canceled } from 'vs/base/common/errors'; import { isWindows } from 'vs/base/common/platform'; export type ValueCallback = (value: T | Promise) => void; -export class DeferredPromise { - - private completeCallback!: ValueCallback; - private errorCallback!: (err: any) => void; - - public p: Promise; - - constructor() { - this.p = new Promise((c, e) => { - this.completeCallback = c; - this.errorCallback = e; - }); - } - - public complete(value: T) { - return new Promise(resolve => { - this.completeCallback(value); - resolve(); - }); - } - - public error(err: any) { - return new Promise(resolve => { - this.errorCallback(err); - resolve(); - }); - } - - public cancel() { - new Promise(resolve => { - this.errorCallback(canceled()); - resolve(); - }); - } -} - export function toResource(this: any, path: string) { if (isWindows) { return URI.file(join('C:\\', btoa(this.test.fullTitle()), path)); @@ -60,7 +23,7 @@ export function suiteRepeat(n: number, description: string, callback: (this: any } } -export function testRepeat(n: number, description: string, callback: (this: any, done: MochaDone) => any): void { +export function testRepeat(n: number, description: string, callback: (this: any) => any): void { for (let i = 0; i < n; i++) { test(`${description} (iteration ${i})`, callback); } diff --git a/src/vs/base/test/common/uuid.test.ts b/src/vs/base/test/common/uuid.test.ts index 6870813f36..b2b317ae75 100644 --- a/src/vs/base/test/common/uuid.test.ts +++ b/src/vs/base/test/common/uuid.test.ts @@ -8,8 +8,8 @@ import * as uuid from 'vs/base/common/uuid'; suite('UUID', () => { test('generation', () => { const asHex = uuid.generateUuid(); - assert.equal(asHex.length, 36); - assert.equal(asHex[14], '4'); + assert.strictEqual(asHex.length, 36); + assert.strictEqual(asHex[14], '4'); assert.ok(asHex[19] === '8' || asHex[19] === '9' || asHex[19] === 'a' || asHex[19] === 'b'); }); diff --git a/src/vs/base/test/node/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index 82d112dc9f..44e1bab01d 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -4,24 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { checksum } from 'vs/base/node/crypto'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, RimRafMode, writeFile } from 'vs/base/node/pfs'; +import { promises } from 'fs'; +import { rimraf, writeFile } from 'vs/base/node/pfs'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Crypto', () => { + let testDir: string; + + setup(function () { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); + + return promises.mkdir(testDir, { recursive: true }); + }); + + teardown(function () { + return rimraf(testDir); + }); + test('checksum', async () => { - const id = generateUuid(); - const testDir = join(tmpdir(), 'vsctests', id); const testFile = join(testDir, 'checksum.txt'); - - await mkdirp(testDir); - await writeFile(testFile, 'Hello World'); await checksum(testFile, '0a4d55a8d778e5022fab701977c5d840bbc486d0'); - - await rimraf(testDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/base/test/node/decoder.test.ts b/src/vs/base/test/node/decoder.test.ts index 56922700a7..e2554685f1 100644 --- a/src/vs/base/test/node/decoder.test.ts +++ b/src/vs/base/test/node/decoder.test.ts @@ -4,19 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as decoder from 'vs/base/node/decoder'; +import { LineDecoder } from 'vs/base/node/decoder'; suite('Decoder', () => { test('decoding', () => { - const lineDecoder = new decoder.LineDecoder(); + const lineDecoder = new LineDecoder(); let res = lineDecoder.write(Buffer.from('hello')); - assert.equal(res.length, 0); + assert.strictEqual(res.length, 0); res = lineDecoder.write(Buffer.from('\nworld')); - assert.equal(res[0], 'hello'); - assert.equal(res.length, 1); + assert.strictEqual(res[0], 'hello'); + assert.strictEqual(res.length, 1); - assert.equal(lineDecoder.end(), 'world'); + assert.strictEqual(lineDecoder.end(), 'world'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index 0359aca322..5042565b7c 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -4,70 +4,53 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; +import { tmpdir } from 'os'; +import { promises } from 'fs'; +import { rimraf } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -suite('Extpath', () => { +flakySuite('Extpath', () => { + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); + + return promises.mkdir(testDir, { recursive: true }); + }); + + teardown(() => { + return rimraf(testDir); + }); test('realcase', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); // assume case insensitive file system if (process.platform === 'win32' || process.platform === 'darwin') { - const upper = newDir.toUpperCase(); + const upper = testDir.toUpperCase(); const real = realcaseSync(upper); if (real) { // can be null in case of permission errors - assert.notEqual(real, upper); - assert.equal(real.toUpperCase(), upper); - assert.equal(real, newDir); + assert.notStrictEqual(real, upper); + assert.strictEqual(real.toUpperCase(), upper); + assert.strictEqual(real, testDir); } } // linux, unix, etc. -> assume case sensitive file system else { - const real = realcaseSync(newDir); - assert.equal(real, newDir); + const real = realcaseSync(testDir); + assert.strictEqual(real, testDir); } - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); test('realpath', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); - - const realpathVal = await realpath(newDir); + const realpathVal = await realpath(testDir); assert.ok(realpathVal); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); }); - test('realpathSync', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'extpath', id); - - await pfs.mkdirp(newDir, 493); - - let realpath!: string; - try { - realpath = realpathSync(newDir); - } catch (error) { - assert.ok(!error); - } - assert.ok(realpath!); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + test('realpathSync', () => { + const realpath = realpathSync(testDir); + assert.ok(realpath); }); }); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index df2e904aef..a648ac2c8b 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -2,22 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { getMachineId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; +import { flakySuite } from 'vs/base/test/node/testUtils'; -suite('ID', () => { +flakySuite('ID', () => { - test('getMachineId', function () { - this.timeout(20000); - return getMachineId().then(id => { - assert.ok(id); - }); + test('getMachineId', async function () { + const id = await getMachineId(); + assert.ok(id); }); - test('getMac', () => { - return getMac().then(macAddress => { - assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); - }); + test('getMac', async () => { + const macAddress = await getMac(); + assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/node/keytar.test.ts b/src/vs/base/test/node/keytar.test.ts index f0c0c0804c..aa1528a5c9 100644 --- a/src/vs/base/test/node/keytar.test.ts +++ b/src/vs/base/test/node/keytar.test.ts @@ -2,36 +2,30 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; +import { isLinux } from 'vs/base/common/platform'; suite('Keytar', () => { - test('loads and is functional', function (done) { - if (platform.isLinux) { - // Skip test due to set up issue with Travis. - this.skip(); - return; - } - (async () => { - const keytar = await import('keytar'); - const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`; + (isLinux ? test.skip : test)('loads and is functional', async () => { // TODO@RMacfarlane test seems to fail on Linux (Error: Unknown or unsupported transport 'disabled' for address 'disabled:') + const keytar = await import('keytar'); + const name = `VSCode Test ${Math.floor(Math.random() * 1e9)}`; + try { + await keytar.setPassword(name, 'foo', 'bar'); + assert.strictEqual(await keytar.findPassword(name), 'bar'); + assert.strictEqual((await keytar.findCredentials(name)).length, 1); + assert.strictEqual(await keytar.getPassword(name, 'foo'), 'bar'); + await keytar.deletePassword(name, 'foo'); + assert.strictEqual(await keytar.getPassword(name, 'foo'), null); + } catch (err) { + // try to clean up try { - await keytar.setPassword(name, 'foo', 'bar'); - assert.equal(await keytar.findPassword(name), 'bar'); - assert.equal((await keytar.findCredentials(name)).length, 1); - assert.equal(await keytar.getPassword(name, 'foo'), 'bar'); await keytar.deletePassword(name, 'foo'); - assert.equal(await keytar.getPassword(name, 'foo'), undefined); - } catch (err) { - // try to clean up - try { - await keytar.deletePassword(name, 'foo'); - } finally { - // eslint-disable-next-line no-unsafe-finally - throw err; - } + } finally { + // eslint-disable-next-line no-unsafe-finally + throw err; } - })().then(done, done); + } }); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index d53916cb5c..23ebc8b05b 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -4,388 +4,373 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; import * as fs from 'fs'; -import * as uuid from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; +import { tmpdir } from 'os'; +import { join, sep } from 'vs/base/common/path'; +import { generateUuid } from 'vs/base/common/uuid'; +import { copy, exists, move, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { isWindows } from 'vs/base/common/platform'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { isWindows } from 'vs/base/common/platform'; -suite('PFS', function () { +flakySuite('PFS', function () { - // Given issues such as https://github.com/microsoft/vscode/issues/84066 - // we see random test failures when accessing the native file system. To - // diagnose further, we retry node.js file access tests up to 3 times to - // rule out any random disk issue. - this.retries(3); + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); + + return fs.promises.mkdir(testDir, { recursive: true }); + }); + + teardown(() => { + return rimraf(testDir); + }); test('writeFile', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); + const testFile = join(testDir, 'writefile.txt'); - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + assert.ok(!(await exists(testFile))); - await pfs.writeFile(testFile, 'Hello World', (null!)); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + await writeFile(testFile, 'Hello World', (null!)); - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile1 = path.join(newDir, 'writefile1.txt'); - const testFile2 = path.join(newDir, 'writefile2.txt'); - const testFile3 = path.join(newDir, 'writefile3.txt'); - const testFile4 = path.join(newDir, 'writefile4.txt'); - const testFile5 = path.join(newDir, 'writefile5.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + const testFile1 = join(testDir, 'writefile1.txt'); + const testFile2 = join(testDir, 'writefile2.txt'); + const testFile3 = join(testDir, 'writefile3.txt'); + const testFile4 = join(testDir, 'writefile4.txt'); + const testFile5 = join(testDir, 'writefile5.txt'); await Promise.all([ - pfs.writeFile(testFile1, 'Hello World 1', (null!)), - pfs.writeFile(testFile2, 'Hello World 2', (null!)), - pfs.writeFile(testFile3, 'Hello World 3', (null!)), - pfs.writeFile(testFile4, 'Hello World 4', (null!)), - pfs.writeFile(testFile5, 'Hello World 5', (null!)) + writeFile(testFile1, 'Hello World 1', (null!)), + writeFile(testFile2, 'Hello World 2', (null!)), + writeFile(testFile3, 'Hello World 3', (null!)), + writeFile(testFile4, 'Hello World 4', (null!)), + writeFile(testFile5, 'Hello World 5', (null!)) ]); - assert.equal(fs.readFileSync(testFile1), 'Hello World 1'); - assert.equal(fs.readFileSync(testFile2), 'Hello World 2'); - assert.equal(fs.readFileSync(testFile3), 'Hello World 3'); - assert.equal(fs.readFileSync(testFile4), 'Hello World 4'); - assert.equal(fs.readFileSync(testFile5), 'Hello World 5'); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual(fs.readFileSync(testFile1).toString(), 'Hello World 1'); + assert.strictEqual(fs.readFileSync(testFile2).toString(), 'Hello World 2'); + assert.strictEqual(fs.readFileSync(testFile3).toString(), 'Hello World 3'); + assert.strictEqual(fs.readFileSync(testFile4).toString(), 'Hello World 4'); + assert.strictEqual(fs.readFileSync(testFile5).toString(), 'Hello World 5'); }); test('writeFile - parallel write on same files works and is sequentalized', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'writefile.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + const testFile = join(testDir, 'writefile.txt'); await Promise.all([ - pfs.writeFile(testFile, 'Hello World 1', undefined), - pfs.writeFile(testFile, 'Hello World 2', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 3', undefined)), - pfs.writeFile(testFile, 'Hello World 4', undefined), - timeout(10).then(() => pfs.writeFile(testFile, 'Hello World 5', undefined)) + writeFile(testFile, 'Hello World 1', undefined), + writeFile(testFile, 'Hello World 2', undefined), + timeout(10).then(() => writeFile(testFile, 'Hello World 3', undefined)), + writeFile(testFile, 'Hello World 4', undefined), + timeout(10).then(() => writeFile(testFile, 'Hello World 5', undefined)) ]); - assert.equal(fs.readFileSync(testFile), 'Hello World 5'); - - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World 5'); }); test('rimraf - simple - unlink', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple - move', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - recursive folder structure - unlink', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - await pfs.rimraf(newDir); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - recursive folder structure - move', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple ends with dot - move', async () => { - const id = `${uuid.generateUuid()}.`; - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(newDir, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(testDir, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimraf - simple ends with dot slash/backslash - move', async () => { - const id = `${uuid.generateUuid()}.`; - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - await pfs.rimraf(`${newDir}${path.sep}`, pfs.RimRafMode.MOVE); - assert.ok(!fs.existsSync(newDir)); + await rimraf(`${testDir}${sep}`, RimRafMode.MOVE); + assert.ok(!fs.existsSync(testDir)); }); test('rimrafSync - swallows file not found error', function () { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + const nonExistingDir = join(testDir, 'not-existing'); + rimrafSync(nonExistingDir); - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(nonExistingDir)); }); test('rimrafSync - simple', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); + rimrafSync(testDir); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); + assert.ok(!fs.existsSync(testDir)); }); test('rimrafSync - recursive folder structure', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); + fs.mkdirSync(join(testDir, 'somefolder')); + fs.writeFileSync(join(testDir, 'somefolder', 'somefile.txt'), 'Contents'); - fs.mkdirSync(path.join(newDir, 'somefolder')); - fs.writeFileSync(path.join(newDir, 'somefolder', 'somefile.txt'), 'Contents'); + rimrafSync(testDir); - pfs.rimrafSync(newDir); - - assert.ok(!fs.existsSync(newDir)); - }); - - test('moveIgnoreError', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - - await pfs.mkdirp(newDir, 493); - try { - await pfs.renameIgnoreError(path.join(newDir, 'foo'), path.join(newDir, 'bar')); - return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); - } - catch (error) { - assert.fail(error); - } + assert.ok(!fs.existsSync(testDir)); }); test('copy, move and delete', async () => { - const id = uuid.generateUuid(); - const id2 = uuid.generateUuid(); + const id = generateUuid(); + const id2 = generateUuid(); const sourceDir = getPathFromAmdModule(require, './fixtures'); - const parentDir = path.join(os.tmpdir(), 'vsctests', 'pfs'); - const targetDir = path.join(parentDir, id); - const targetDir2 = path.join(parentDir, id2); + const parentDir = join(tmpdir(), 'vsctests', 'pfs'); + const targetDir = join(parentDir, id); + const targetDir2 = join(parentDir, id2); - await pfs.copy(sourceDir, targetDir); + await copy(sourceDir, targetDir, { preserveSymlinks: true }); assert.ok(fs.existsSync(targetDir)); - assert.ok(fs.existsSync(path.join(targetDir, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir, 'examples', 'small.jxs'))); + assert.ok(fs.existsSync(join(targetDir, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir, 'site.css'))); + assert.ok(fs.existsSync(join(targetDir, 'examples'))); + assert.ok(fs.statSync(join(targetDir, 'examples')).isDirectory()); + assert.ok(fs.existsSync(join(targetDir, 'examples', 'small.jxs'))); - await pfs.move(targetDir, targetDir2); + await move(targetDir, targetDir2); assert.ok(!fs.existsSync(targetDir)); assert.ok(fs.existsSync(targetDir2)); - assert.ok(fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'site.css'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples'))); - assert.ok(fs.statSync(path.join(targetDir2, 'examples')).isDirectory()); - assert.ok(fs.existsSync(path.join(targetDir2, 'examples', 'small.jxs'))); + assert.ok(fs.existsSync(join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir2, 'site.css'))); + assert.ok(fs.existsSync(join(targetDir2, 'examples'))); + assert.ok(fs.statSync(join(targetDir2, 'examples')).isDirectory()); + assert.ok(fs.existsSync(join(targetDir2, 'examples', 'small.jxs'))); - await pfs.move(path.join(targetDir2, 'index.html'), path.join(targetDir2, 'index_moved.html')); + await move(join(targetDir2, 'index.html'), join(targetDir2, 'index_moved.html')); - assert.ok(!fs.existsSync(path.join(targetDir2, 'index.html'))); - assert.ok(fs.existsSync(path.join(targetDir2, 'index_moved.html'))); + assert.ok(!fs.existsSync(join(targetDir2, 'index.html'))); + assert.ok(fs.existsSync(join(targetDir2, 'index_moved.html'))); - await pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + await rimraf(parentDir); assert.ok(!fs.existsSync(parentDir)); }); - test('mkdirp', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + test('copy handles symbolic links', async () => { + const id1 = generateUuid(); + const symbolicLinkTarget = join(testDir, id1); - await pfs.mkdirp(newDir, 493); + const id2 = generateUuid(); + const symLink = join(testDir, id2); - assert.ok(fs.existsSync(newDir)); + const id3 = generateUuid(); + const copyTarget = join(testDir, id3); - return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE); + await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); + + fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); + + // Copy preserves symlinks if configured as such + // + // Windows: this test does not work because creating symlinks + // requires priviledged permissions (admin). + if (!isWindows) { + await copy(symLink, copyTarget, { preserveSymlinks: true }); + + assert.ok(fs.existsSync(copyTarget)); + + const { symbolicLink } = await SymlinkSupport.stat(copyTarget); + assert.ok(symbolicLink); + assert.ok(!symbolicLink.dangling); + + const target = await fs.promises.readlink(copyTarget); + assert.strictEqual(target, symbolicLinkTarget); + + // Copy does not preserve symlinks if configured as such + + await rimraf(copyTarget); + await copy(symLink, copyTarget, { preserveSymlinks: false }); + + assert.ok(fs.existsSync(copyTarget)); + + const { symbolicLink: symbolicLink2 } = await SymlinkSupport.stat(copyTarget); + assert.ok(!symbolicLink2); + } + + // Copy ignores dangling symlinks + + await rimraf(copyTarget); + await rimraf(symbolicLinkTarget); + + await copy(symLink, copyTarget, { preserveSymlinks: true }); // this should not throw + + assert.ok(!fs.existsSync(copyTarget)); + }); + + test('copy handles symbolic links when the reference is inside source', async () => { + + // Source Folder + const sourceFolder = join(testDir, generateUuid(), 'copy-test'); // copy-test + const sourceLinkTestFolder = join(sourceFolder, 'link-test'); // copy-test/link-test + const sourceLinkMD5JSFolder = join(sourceLinkTestFolder, 'md5'); // copy-test/link-test/md5 + const sourceLinkMD5JSFile = join(sourceLinkMD5JSFolder, 'md5.js'); // copy-test/link-test/md5/md5.js + await fs.promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); + await writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); + + const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked + fs.symlinkSync(sourceLinkMD5JSFolder, sourceLinkMD5JSFolderLinked, 'junction'); + + // Target Folder + const targetLinkTestFolder = join(sourceFolder, 'link-test copy'); // copy-test/link-test copy + const targetLinkMD5JSFolder = join(targetLinkTestFolder, 'md5'); // copy-test/link-test copy/md5 + const targetLinkMD5JSFile = join(targetLinkMD5JSFolder, 'md5.js'); // copy-test/link-test copy/md5/md5.js + const targetLinkMD5JSFolderLinked = join(targetLinkTestFolder, 'md5-linked'); // copy-test/link-test copy/md5-linked + + // Copy with `preserveSymlinks: true` and verify result + // + // Windows: this test does not work because creating symlinks + // requires priviledged permissions (admin). + if (!isWindows) { + await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true }); + + assert.ok(fs.existsSync(targetLinkTestFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFile)); + assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); + assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); + + const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); + assert.strictEqual(linkTarget, targetLinkMD5JSFolder); + + await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + } + + // Copy with `preserveSymlinks: false` and verify result + await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: false }); + + assert.ok(fs.existsSync(targetLinkTestFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFolder)); + assert.ok(fs.existsSync(targetLinkMD5JSFile)); + assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); + assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isDirectory()); }); test('readDirsInDir', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); + fs.mkdirSync(join(testDir, 'somefolder1')); + fs.mkdirSync(join(testDir, 'somefolder2')); + fs.mkdirSync(join(testDir, 'somefolder3')); + fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents'); + fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents'); - await pfs.mkdirp(newDir, 493); - - fs.mkdirSync(path.join(newDir, 'somefolder1')); - fs.mkdirSync(path.join(newDir, 'somefolder2')); - fs.mkdirSync(path.join(newDir, 'somefolder3')); - fs.writeFileSync(path.join(newDir, 'somefile.txt'), 'Contents'); - fs.writeFileSync(path.join(newDir, 'someOtherFile.txt'), 'Contents'); - - const result = await pfs.readDirsInDir(newDir); - assert.equal(result.length, 3); + const result = await readDirsInDir(testDir); + assert.strictEqual(result.length, 3); assert.ok(result.indexOf('somefolder1') !== -1); assert.ok(result.indexOf('somefolder2') !== -1); assert.ok(result.indexOf('somefolder3') !== -1); - - await pfs.rimraf(newDir); }); test('stat link', async () => { - if (isWindows) { - return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges - } + const id1 = generateUuid(); + const directory = join(testDir, id1); - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'pfs', id1); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'pfs', id2); + await fs.promises.mkdir(directory, { recursive: true }); - await pfs.mkdirp(directory, 493); + fs.symlinkSync(directory, symbolicLink, 'junction'); - fs.symlinkSync(directory, symbolicLink); - - let statAndIsLink = await pfs.statLink(directory); + let statAndIsLink = await SymlinkSupport.stat(directory); assert.ok(!statAndIsLink?.symbolicLink); - statAndIsLink = await pfs.statLink(symbolicLink); + statAndIsLink = await SymlinkSupport.stat(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(!statAndIsLink?.symbolicLink?.dangling); - - pfs.rimrafSync(directory); }); test('stat link (non existing target)', async () => { - if (isWindows) { - return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges - } + const id1 = generateUuid(); + const directory = join(testDir, id1); - const id1 = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id1); - const directory = path.join(parentDir, 'pfs', id1); + const id2 = generateUuid(); + const symbolicLink = join(testDir, id2); - const id2 = uuid.generateUuid(); - const symbolicLink = path.join(parentDir, 'pfs', id2); + await fs.promises.mkdir(directory, { recursive: true }); - await pfs.mkdirp(directory, 493); + fs.symlinkSync(directory, symbolicLink, 'junction'); - fs.symlinkSync(directory, symbolicLink); + await rimraf(directory); - pfs.rimrafSync(directory); - - const statAndIsLink = await pfs.statLink(symbolicLink); + const statAndIsLink = await SymlinkSupport.stat(symbolicLink); assert.ok(statAndIsLink?.symbolicLink); assert.ok(statAndIsLink?.symbolicLink?.dangling); }); test('readdir', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id, 'öäü'); + const id = generateUuid(); + const newDir = join(testDir, 'pfs', id, 'öäü'); - await pfs.mkdirp(newDir, 493); + await fs.promises.mkdir(newDir, { recursive: true }); assert.ok(fs.existsSync(newDir)); - const children = await pfs.readdir(path.join(parentDir, 'pfs', id)); - assert.equal(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so - - await pfs.rimraf(parentDir); + const children = await readdir(join(testDir, 'pfs', id)); + assert.strictEqual(children.some(n => n === 'öäü'), true); // Mac always converts to NFD, so } }); - test('readdirWithFileTypes', async () => { + test('readdir (with file types)', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const testDir = path.join(parentDir, 'pfs', id); + const newDir = join(testDir, 'öäü'); + await fs.promises.mkdir(newDir, { recursive: true }); - const newDir = path.join(testDir, 'öäü'); - await pfs.mkdirp(newDir, 493); - - await pfs.writeFile(path.join(testDir, 'somefile.txt'), 'contents'); + await writeFile(join(testDir, 'somefile.txt'), 'contents'); assert.ok(fs.existsSync(newDir)); - const children = await pfs.readdirWithFileTypes(testDir); + const children = await readdir(testDir, { withFileTypes: true }); - assert.equal(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so - assert.equal(children.some(n => n.isDirectory()), true); + assert.strictEqual(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so + assert.strictEqual(children.some(n => n.isDirectory()), true); - assert.equal(children.some(n => n.name === 'somefile.txt'), true); - assert.equal(children.some(n => n.isFile()), true); - - await pfs.rimraf(parentDir); + assert.strictEqual(children.some(n => n.name === 'somefile.txt'), true); + assert.strictEqual(children.some(n => n.isFile()), true); } }); @@ -416,65 +401,41 @@ suite('PFS', function () { bigData: string | Buffer | Uint8Array, bigDataValue: string ): Promise { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); + assert.ok(fs.existsSync(testDir)); - await pfs.writeFile(testFile, smallData); - assert.equal(fs.readFileSync(testFile), smallDataValue); + await writeFile(testFile, smallData); + assert.strictEqual(fs.readFileSync(testFile).toString(), smallDataValue); - await pfs.writeFile(testFile, bigData); - assert.equal(fs.readFileSync(testFile), bigDataValue); - - await pfs.rimraf(parentDir); + await writeFile(testFile, bigData); + assert.strictEqual(fs.readFileSync(testFile).toString(), bigDataValue); } test('writeFile (string, error handling)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + fs.mkdirSync(testFile); // this will trigger an error later because testFile is now a directory! let expectedError: Error | undefined; try { - await pfs.writeFile(testFile, 'Hello World'); + await writeFile(testFile, 'Hello World'); } catch (error) { expectedError = error; } assert.ok(expectedError); - - await pfs.rimraf(parentDir); }); test('writeFileSync', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); + const testFile = join(testDir, 'flushed.txt'); - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - pfs.writeFileSync(testFile, 'Hello World'); - assert.equal(fs.readFileSync(testFile), 'Hello World'); + writeFileSync(testFile, 'Hello World'); + assert.strictEqual(fs.readFileSync(testFile).toString(), 'Hello World'); const largeString = (new Array(100 * 1024)).join('Large String\n'); - pfs.writeFileSync(testFile, largeString); - assert.equal(fs.readFileSync(testFile), largeString); - - await pfs.rimraf(parentDir); + writeFileSync(testFile, largeString); + assert.strictEqual(fs.readFileSync(testFile).toString(), largeString); }); }); diff --git a/src/vs/base/test/node/port.test.ts b/src/vs/base/test/node/port.test.ts index c4ec1a439a..b813bc58fa 100644 --- a/src/vs/base/test/node/port.test.ts +++ b/src/vs/base/test/node/port.test.ts @@ -6,14 +6,10 @@ import * as assert from 'assert'; import * as net from 'net'; import * as ports from 'vs/base/node/ports'; +import { flakySuite } from 'vs/base/test/node/testUtils'; -suite('Ports', () => { - test('Finds a free port (no timeout)', function (done) { - this.timeout(1000 * 10); // higher timeout for this test - - if (process.env['VSCODE_PID']) { - return done(); // this test fails when run from within VS Code - } +flakySuite('Ports', () => { + (process.env['VSCODE_PID'] ? test.skip /* this test fails when run from within VS Code */ : test)('Finds a free port (no timeout)', function (done) { // get an initial freeport >= 7000 ports.findFreePort(7000, 100, 300000).then(initialPort => { diff --git a/src/vs/base/test/node/powershell.test.ts b/src/vs/base/test/node/powershell.test.ts new file mode 100644 index 0000000000..4cea0b2642 --- /dev/null +++ b/src/vs/base/test/node/powershell.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import { enumeratePowerShellInstallations, getFirstAvailablePowerShellInstallation, IPowerShellExeDetails } from 'vs/base/node/powershell'; + +function checkPath(exePath: string) { + // Check to see if the path exists + let pathCheckResult = false; + try { + const stat = fs.statSync(exePath); + pathCheckResult = stat.isFile(); + } catch { + // fs.exists throws on Windows with SymbolicLinks so we + // also use lstat to try and see if the file exists. + try { + pathCheckResult = fs.statSync(fs.readlinkSync(exePath)).isFile(); + } catch { + + } + } + + assert.strictEqual(pathCheckResult, true); +} + +if (platform.isWindows) { + suite('PowerShell finder', () => { + + test('Can find first available PowerShell', async () => { + const pwshExe = await getFirstAvailablePowerShellInstallation(); + const exePath = pwshExe?.exePath; + assert.notStrictEqual(exePath, null); + assert.notStrictEqual(pwshExe?.displayName, null); + + checkPath(exePath!); + }); + + test('Can enumerate PowerShells', async () => { + const isOS64Bit = os.arch() === 'x64'; + const pwshs = new Array(); + for await (const p of enumeratePowerShellInstallations()) { + pwshs.push(p); + } + + const powershellLog = 'Found these PowerShells:\n' + pwshs.map(p => `${p.displayName}: ${p.exePath}`).join('\n'); + assert.strictEqual(pwshs.length >= (isOS64Bit ? 2 : 1), true, powershellLog); + + for (const pwsh of pwshs) { + checkPath(pwsh.exePath); + } + + + const lastIndex = pwshs.length - 1; + const secondToLastIndex = pwshs.length - 2; + + // 64bit process on 64bit OS + if (process.arch === 'x64') { + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell', powershellLog); + + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + } else if (isOS64Bit) { + // 32bit process on 64bit OS + + // Windows PowerShell x86 comes first if vscode is 32bit + checkPath(pwshs[secondToLastIndex].exePath); + assert.strictEqual(pwshs[secondToLastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell', powershellLog); + } else { + // 32bit or ARM process + checkPath(pwshs[lastIndex].exePath); + assert.strictEqual(pwshs[lastIndex].displayName, 'Windows PowerShell (x86)', powershellLog); + } + }); + }); +} diff --git a/src/vs/base/test/node/processes/processes.test.ts b/src/vs/base/test/node/processes/processes.test.ts index d32e2e5485..931fcfef27 100644 --- a/src/vs/base/test/node/processes/processes.test.ts +++ b/src/vs/base/test/node/processes/processes.test.ts @@ -13,9 +13,9 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; function fork(id: string): cp.ChildProcess { const opts: any = { env: objects.mixin(objects.deepClone(process.env), { - AMD_ENTRYPOINT: id, - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: true + VSCODE_AMD_ENTRYPOINT: id, + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: true }) }; @@ -59,11 +59,7 @@ suite('Processes', () => { }); }); - test('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { - if (!platform.isWindows || process.env['VSCODE_PID']) { - return done(); // test is only relevant for Windows and seems to crash randomly on some Linux builds - } - + (!platform.isWindows || process.env['VSCODE_PID'] ? test.skip : test)('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { // test is only relevant for Windows and seems to crash randomly on some Linux builds const child = fork('vs/base/test/node/processes/fixtures/fork_large'); const sender = processes.createQueuedSender(child); diff --git a/src/vs/base/test/node/testUtils.ts b/src/vs/base/test/node/testUtils.ts index aebb56a418..97ba21c9f9 100644 --- a/src/vs/base/test/node/testUtils.ts +++ b/src/vs/base/test/node/testUtils.ts @@ -3,9 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { Suite } from 'mocha'; import { join } from 'vs/base/common/path'; import { generateUuid } from 'vs/base/common/uuid'; export function getRandomTestPath(tmpdir: string, ...segments: string[]): string { return join(tmpdir, ...segments, generateUuid()); } + +export function flakySuite(title: string, fn: (this: Suite) => void): Suite { + return suite(title, function () { + + // Flaky suites need retries and timeout to complete + // e.g. because they access the file system which can + // be unreliable depending on the environment. + this.retries(3); + this.timeout(1000 * 20); + + // Invoke suite ensuring that `this` is + // properly wired in. + fn.call(this); + }); +} diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index 5743def405..b4381e157f 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -5,24 +5,34 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import * as os from 'os'; +import { tmpdir } from 'os'; +import { promises } from 'fs'; import { extract } from 'vs/base/node/zip'; -import { generateUuid } from 'vs/base/common/uuid'; import { rimraf, exists } from 'vs/base/node/pfs'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { createCancelablePromise } from 'vs/base/common/async'; - -const fixtures = getPathFromAmdModule(require, './fixtures'); +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; suite('Zip', () => { - test('extract should handle directories', () => { - const fixture = path.join(fixtures, 'extract.zip'); - const target = path.join(os.tmpdir(), generateUuid()); + let testDir: string; - return createCancelablePromise(token => extract(fixture, target, {}, token) - .then(() => exists(path.join(target, 'extension'))) - .then(exists => assert(exists)) - .then(() => rimraf(target))); + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); + + return promises.mkdir(testDir, { recursive: true }); + }); + + teardown(() => { + return rimraf(testDir); + }); + + test('extract should handle directories', async () => { + const fixtures = getPathFromAmdModule(require, './fixtures'); + const fixture = path.join(fixtures, 'extract.zip'); + + await createCancelablePromise(token => extract(fixture, testDir, {}, token)); + const doesExist = await exists(path.join(testDir, 'extension')); + assert(doesExist); }); }); diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index 916e47d6ce..7131cb32c4 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -6,6 +6,8 @@ import { globals } from 'vs/base/common/platform'; import { IWorker, IWorkerCallback, IWorkerFactory, logOnceWebWorkerWarning } from 'vs/base/common/worker/simpleWorker'; +const ttPolicy = window.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value }); + function getWorker(workerId: string, label: string): Worker | Promise { // Option for hosts to overwrite the worker script (used in the standalone editor) if (globals.MonacoEnvironment) { @@ -13,7 +15,8 @@ function getWorker(workerId: string, label: string): Worker | Promise { return globals.MonacoEnvironment.getWorker(workerId, label); } if (typeof globals.MonacoEnvironment.getWorkerUrl === 'function') { - return new Worker(globals.MonacoEnvironment.getWorkerUrl(workerId, label)); + const wokerUrl = globals.MonacoEnvironment.getWorkerUrl(workerId, label); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(wokerUrl) as unknown as string : wokerUrl, { name: label }); } } // ESM-comment-begin @@ -21,7 +24,7 @@ function getWorker(workerId: string, label: string): Worker | Promise { // check if the JS lives on a different origin const workerMain = require.toUrl('./' + workerId); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321 const workerUrl = getWorkerBootstrapUrl(workerMain, label); - return new Worker(workerUrl, { name: label }); + return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label }); } // ESM-comment-end throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`); diff --git a/src/vs/buildunit.json b/src/vs/buildunit.json deleted file mode 100644 index 88fe75c555..0000000000 --- a/src/vs/buildunit.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "vs", - "dependencies": [ - ], - "libs": [ - "lib.core.d.ts" - ], - "sources": [ - ], - "declares": [ - "vs/nls.d.ts" - ] -} \ No newline at end of file diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index c6af27976a..2c862b66bb 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -3,8 +3,7 @@ @@ -106,7 +105,7 @@ @@ -92,7 +91,7 @@ diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index fc8c724f8e..6429a0575f 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -265,7 +265,6 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi setTimeout(() => this.periodicFetchCallback(requestId, startTime), PollingURLCallbackProvider.FETCH_INTERVAL); } } - } class WorkspaceProvider implements IWorkspaceProvider { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts new file mode 100644 index 0000000000..c0be610c48 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; + +export class DeprecatedExtensionsCleaner extends Disposable { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: ExtensionManagementService + ) { + super(); + + this._register(extensionManagementService); // TODO@sandy081 this seems fishy + + this.cleanUpDeprecatedExtensions(); + } + + private cleanUpDeprecatedExtensions(): void { + this.extensionManagementService.removeDeprecatedExtensions(); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 3da7d91cba..c852e222c6 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -52,7 +53,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months try { const installed: IStringDictionary = Object.create(null); - const metaData: LanguagePackFile = JSON.parse(await pfs.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: LanguagePackFile = JSON.parse(await fs.promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); for (let locale of Object.keys(metaData)) { const entry = metaData[locale]; installed[`${entry.hash}.${locale}`] = true; @@ -80,7 +81,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { continue; } const candidate = path.join(folder, entry); - const stat = await pfs.stat(candidate); + const stat = await fs.promises.stat(candidate); if (stat.isDirectory()) { const diff = now - stat.mtime.getTime(); if (diff > maxAge) { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts new file mode 100644 index 0000000000..382b802081 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; + +export class LocalizationsUpdater extends Disposable { + + constructor( + @ILocalizationsService private readonly localizationsService: LocalizationsService + ) { + super(); + + this.updateLocalizations(); + } + + private updateLocalizations(): void { + this.localizationsService.update(); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts index 290adb27d3..903075e2b1 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts @@ -8,6 +8,7 @@ import { join, dirname, basename } from 'vs/base/common/path'; import { readdir, rimraf } from 'vs/base/node/pfs'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Promises } from 'vs/base/common/async'; export class LogsDataCleaner extends Disposable { @@ -31,7 +32,7 @@ export class LogsDataCleaner extends Disposable { const oldSessions = allSessions.sort().filter((d, i) => d !== currentLog); const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); - return Promise.all(toDelete.map(name => rimraf(join(logsRoot, name)))); + return Promises.settled(toDelete.map(name => rimraf(join(logsRoot, name)))); }).then(null, onUnexpectedError); }, 10 * 1000); diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts index ffa241b5be..ea80ef93c3 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts @@ -3,10 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { basename, dirname, join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { readdir, rimraf, stat } from 'vs/base/node/pfs'; +import { readdir, rimraf } from 'vs/base/node/pfs'; import product from 'vs/platform/product/common/product'; export class NodeCachedDataCleaner { @@ -54,7 +55,7 @@ export class NodeCachedDataCleaner { if (entry !== nodeCachedDataCurrent) { const path = join(nodeCachedDataRootDir, entry); - deletes.push(stat(path).then(stats => { + deletes.push(promises.stat(path).then(stats => { // stat check // * only directories // * only when old enough diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index 0ecb2bfdc1..41001b5994 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -3,9 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/path'; -import { readdir, readFile, rimraf } from 'vs/base/node/pfs'; +import { readdir, rimraf } from 'vs/base/node/pfs'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup'; @@ -32,7 +33,7 @@ export class StorageDataCleaner extends Disposable { try { // Leverage the backup workspace file to find out which empty workspace is currently in use to // determine which empty workspace storage can safely be deleted - const contents = await readFile(this.backupWorkspacesPath, 'utf8'); + const contents = await promises.readFile(this.backupWorkspacesPath, 'utf8'); const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat; const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(info => info.backupFolder); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index cadc801407..d436860a0a 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -15,10 +15,7 @@ // Load shared process into window bootstrapWindow.load(['vs/code/electron-browser/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) { - sharedProcess.startup({ - machineId: configuration.machineId, - windowId: configuration.windowId - }); + return sharedProcess.main(configuration); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 1a53373ece..1cc5337f0f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -3,15 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as platform from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; -import { serve, Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import * as fs from 'fs'; +import { release } from 'os'; +import { gracefulify } from 'graceful-fs'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; +import { StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -23,24 +24,23 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService, ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; -import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { ILogService, LogLevel, ILoggerService } from 'vs/platform/log/common/log'; +import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogService } from 'vs/platform/log/common/log'; import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; -import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { DownloadService } from 'vs/platform/download/common/downloadService'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { IChannel, IServerChannel, StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -48,7 +48,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration as registerUserDataSyncConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService, IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; @@ -67,146 +67,184 @@ import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-s import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater'; +import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner'; +import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; -export interface ISharedProcessConfiguration { - readonly machineId: string; - readonly windowId: number; -} +class SharedProcessMain extends Disposable { -export function startup(configuration: ISharedProcessConfiguration) { - handshake(configuration); -} + private server = this._register(new MessagePortServer()); -interface ISharedProcessInitData { - sharedIPCHandle: string; - args: NativeParsedArgs; - logLevel: LogLevel; - nodeCachedDataDir?: string; - backupWorkspacesPath: string; -} + constructor(private configuration: ISharedProcessConfiguration) { + super(); -const eventPrefix = 'adsworkbench'; // {{SQL CARBON EDIT}} + // Enable gracefulFs + gracefulify(fs); -class MainProcessService implements IMainProcessService { - - constructor( - private server: Server, - private mainRouter: StaticRouter - ) { } - - declare readonly _serviceBrand: undefined; - - getChannel(channelName: string): IChannel { - return this.server.getChannel(channelName, this.mainRouter); + this.registerListeners(); } - registerChannel(channelName: string, channel: IServerChannel): void { - this.server.registerChannel(channelName, channel); + private registerListeners(): void { + + // Dispose on exit + const onExit = () => this.dispose(); + process.once('exit', onExit); + ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); } -} -async function main(server: Server, initData: ISharedProcessInitData, configuration: ISharedProcessConfiguration): Promise { - const services = new ServiceCollection(); + async open(): Promise { - const disposables = new DisposableStore(); + // Services + const instantiationService = await this.initServices(); - const onExit = () => disposables.dispose(); - process.once('exit', onExit); - ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); + // Config + registerUserDataSyncConfiguration(); - disposables.add(server); + instantiationService.invokeFunction(accessor => { + const logService = accessor.get(ILogService); - const environmentService = new NativeEnvironmentService(initData.args); + // Log info + logService.trace('sharedProcess configuration', JSON.stringify(this.configuration)); - const mainRouter = new StaticRouter(ctx => ctx === 'main'); - const loggerClient = new LoggerChannelClient(server.getChannel('logger', mainRouter)); - const logService = new FollowerLogService(loggerClient, new SpdLogService('sharedprocess', environmentService.logsPath, initData.logLevel)); - disposables.add(logService); - logService.info('main', JSON.stringify(configuration)); + // Channels + this.initChannels(accessor); - const mainProcessService = new MainProcessService(server, mainRouter); - services.set(IMainProcessService, mainProcessService); + // Error handler + this.registerErrorHandler(logService); + }); - // Files - const fileService = new FileService(logService); - services.set(IFileService, fileService); - disposables.add(fileService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + // Instantiate Contributions + this._register(combinedDisposable( + new NodeCachedDataCleaner(this.configuration.nodeCachedDataDir), + instantiationService.createInstance(LanguagePackCachedDataCleaner), + instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath), + instantiationService.createInstance(LogsDataCleaner), + instantiationService.createInstance(LocalizationsUpdater), + instantiationService.createInstance(DeprecatedExtensionsCleaner) + )); + } - // Configuration - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); - disposables.add(configurationService); - await configurationService.initialize(); - - // Storage - const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); - await storageService.initialize(); - services.set(IStorageService, storageService); - disposables.add(toDisposable(() => storageService.flush())); - - services.set(IEnvironmentService, environmentService); - services.set(INativeEnvironmentService, environmentService); - - services.set(IProductService, { _serviceBrand: undefined, ...product }); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IRequestService, new SyncDescriptor(RequestService)); - services.set(ILoggerService, new SyncDescriptor(LoggerService)); - - const nativeHostService = createChannelSender(mainProcessService.getChannel('nativeHost'), { context: configuration.windowId }); - services.set(INativeHostService, nativeHostService); - const activeWindowManager = new ActiveWindowManager(nativeHostService); - const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); - - services.set(IDownloadService, new SyncDescriptor(DownloadService)); - services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(server.getChannel('IExtensionRecommendationNotificationService', activeWindowRouter))); - - const instantiationService = new InstantiationService(services); - - let telemetryService: ITelemetryService; - instantiationService.invokeFunction(accessor => { + private async initServices(): Promise { const services = new ServiceCollection(); + + // Environment + const environmentService = new NativeEnvironmentService(this.configuration.args); + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); + + // Log + const mainRouter = new StaticRouter(ctx => ctx === 'main'); + const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); // we only use this for log levels + const multiplexLogger = this._register(new MultiplexLogService([ + this._register(new ConsoleLogService(this.configuration.logLevel)), + this._register(new SpdLogService('sharedprocess', environmentService.logsPath, this.configuration.logLevel)) + ])); + + const logService = this._register(new FollowerLogService(loggerClient, multiplexLogger)); + services.set(ILogService, logService); + + // Main Process + const mainProcessService = new MessagePortMainProcessService(this.server, mainRouter); + services.set(IMainProcessService, mainProcessService); + + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); + + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + services.set(IConfigurationService, configurationService); + + await configurationService.initialize(); + + // Storage + const storageService = new NativeStorageService(new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')), logService, environmentService); + services.set(IStorageService, storageService); + + await storageService.initialize(); + this._register(toDisposable(() => storageService.flush())); + + // Product + services.set(IProductService, { _serviceBrand: undefined, ...product }); + + // Request + services.set(IRequestService, new SyncDescriptor(RequestService)); + + // Native Host + const nativeHostService = createChannelSender(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId }); + services.set(INativeHostService, nativeHostService); + + // Download + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + + // Extension recommendations + const activeWindowManager = this._register(new ActiveWindowManager(nativeHostService)); + const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); + services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter))); + + // Logger + const loggerService = this._register(new LoggerService(logService, fileService)); + services.set(ILoggerService, loggerService); + + // Telemetry const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; - let telemetryAppender: ITelemetryAppender = NullAppender; + let telemetryService: ITelemetryService; + let telemetryAppender: ITelemetryAppender; if (!extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { - telemetryAppender = new TelemetryLogAppender(accessor.get(ILoggerService), environmentService); + telemetryAppender = new TelemetryLogAppender(loggerService, environmentService); + + // Application Insights if (product.aiConfig && product.aiConfig.asimovKey && isBuilt) { - const appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey); - disposables.add(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data + const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey); + this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data telemetryAppender = combinedAppender(appInsightsAppender, telemetryAppender); } - const config: ITelemetryServiceConfig = { + + telemetryService = new TelemetryService({ appender: telemetryAppender, - commonProperties: resolveCommonProperties(product.commit, product.version, configuration.machineId, product.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, this.configuration.machineId, product.msftInternalDomains, installSourcePath), sendErrorTelemetry: true, piiPaths: [appRoot, extensionsPath] - }; - - telemetryService = new TelemetryService(config, configurationService); - services.set(ITelemetryService, telemetryService); + }, configurationService); } else { telemetryService = NullTelemetryService; - services.set(ITelemetryService, NullTelemetryService); + telemetryAppender = NullAppender; } - server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + this.server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(telemetryAppender)); + services.set(ITelemetryService, telemetryService); + + // Extension Management services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + // Extension Gallery services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); - services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); - services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + + // Extension Tips services.set(IExtensionTipsService, new SyncDescriptor(ExtensionTipsService)); + // Localizations + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + // Diagnostics + services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); + + // Settings Sync services.set(IUserDataSyncAccountService, new SyncDescriptor(UserDataSyncAccountService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); - services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); + services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(this.server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService)); services.set(IExtensionsStorageSyncService, new SyncDescriptor(ExtensionsStorageSyncService)); @@ -217,114 +255,86 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataAutoSyncEnablementService, new SyncDescriptor(UserDataAutoSyncEnablementService)); services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); - registerConfiguration(); - const instantiationService2 = instantiationService.createChild(services); - - instantiationService2.invokeFunction(accessor => { - - const extensionManagementService = accessor.get(IExtensionManagementService); - const channel = new ExtensionManagementChannel(extensionManagementService, () => null); - server.registerChannel('extensions', channel); - - const localizationsService = accessor.get(ILocalizationsService); - const localizationsChannel = createChannelReceiver(localizationsService); - server.registerChannel('localizations', localizationsChannel); - - const diagnosticsService = accessor.get(IDiagnosticsService); - const diagnosticsChannel = createChannelReceiver(diagnosticsService); - server.registerChannel('diagnostics', diagnosticsChannel); - - const extensionTipsService = accessor.get(IExtensionTipsService); - const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); - server.registerChannel('extensionTipsService', extensionTipsChannel); - - const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); - const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); - server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); - - const authTokenService = accessor.get(IUserDataSyncAccountService); - const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); - server.registerChannel('userDataSyncAccount', authTokenChannel); - - const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); - const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); - server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); - - const userDataSyncService = accessor.get(IUserDataSyncService); - const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService); - server.registerChannel('userDataSync', userDataSyncChannel); - - const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService); - const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); - server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); - - // clean up deprecated extensions - (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); - // update localizations cache - (localizationsService as LocalizationsService).update(); - // cache clean ups - disposables.add(combinedDisposable( - new NodeCachedDataCleaner(initData.nodeCachedDataDir), - instantiationService2.createInstance(LanguagePackCachedDataCleaner), - instantiationService2.createInstance(StorageDataCleaner, initData.backupWorkspacesPath), - instantiationService2.createInstance(LogsDataCleaner), - userDataAutoSync - )); - disposables.add(extensionManagementService as ExtensionManagementService); - }); - }); -} - -function setupIPC(hook: string): Promise { - function setup(retry: boolean): Promise { - return serve(hook).then(null, err => { - if (!retry || platform.isWindows || err.code !== 'EADDRINUSE') { - return Promise.reject(err); - } - - // should retry, not windows and eaddrinuse - - return connect(hook, '').then( - client => { - // we could connect to a running instance. this is not good, abort - client.dispose(); - return Promise.reject(new Error('There is an instance already running.')); - }, - err => { - // it happens on Linux and OS X that the pipe is left behind - // let's delete it, since we can't connect to it - // and the retry the whole thing - try { - fs.unlinkSync(hook); - } catch (e) { - return Promise.reject(new Error('Error deleting the shared ipc hook.')); - } - - return setup(false); - } - ); - }); + return new InstantiationService(services); } - return setup(true); + private initChannels(accessor: ServicesAccessor): void { + + // Extensions Management + const extensionManagementService = accessor.get(IExtensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, () => null); + this.server.registerChannel('extensions', channel); + + // Localizations + const localizationsService = accessor.get(ILocalizationsService); + const localizationsChannel = createChannelReceiver(localizationsService); + this.server.registerChannel('localizations', localizationsChannel); + + // Diagnostics + const diagnosticsService = accessor.get(IDiagnosticsService); + const diagnosticsChannel = createChannelReceiver(diagnosticsService); + this.server.registerChannel('diagnostics', diagnosticsChannel); + + // Extension Tips + const extensionTipsService = accessor.get(IExtensionTipsService); + const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); + this.server.registerChannel('extensionTipsService', extensionTipsChannel); + + // Settings Sync + const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); + const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); + this.server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); + + const authTokenService = accessor.get(IUserDataSyncAccountService); + const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService); + this.server.registerChannel('userDataSyncAccount', authTokenChannel); + + const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService); + const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService); + this.server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel); + + const userDataSyncService = accessor.get(IUserDataSyncService); + const userDataSyncChannel = new UserDataSyncChannel(this.server, userDataSyncService, accessor.get(ILogService)); + this.server.registerChannel('userDataSync', userDataSyncChannel); + + const userDataAutoSync = this._register(accessor.get(IInstantiationService).createInstance(UserDataAutoSyncService)); + const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); + this.server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); + } + + private registerErrorHandler(logService: ILogService): void { + + // Listen on unhandled rejection events + window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + + // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + onUnexpectedError(event.reason); + + // Prevent the printing of this event to the console + event.preventDefault(); + }); + + // Install handler for unexpected errors + setUnexpectedErrorHandler(error => { + const message = toErrorMessage(error, true); + if (!message) { + return; + } + + logService.error(`[uncaught exception in sharedProcess]: ${message}`); + }); + } } -async function handshake(configuration: ISharedProcessConfiguration): Promise { +export async function main(configuration: ISharedProcessConfiguration): Promise { - // receive payload from electron-main to start things - const data = await new Promise(c => { - ipcRenderer.once('vscode:electron-main->shared-process=payload', (event: unknown, r: ISharedProcessInitData) => c(r)); - - // tell electron-main we are ready to receive payload - ipcRenderer.send('vscode:shared-process->electron-main=ready-for-payload'); - }); - - // await IPC connection and signal this back to electron-main - const server = await setupIPC(data.sharedIPCHandle); + // create shared process and signal back to main that we are + // ready to accept message ports as client connections + const sharedProcess = new SharedProcessMain(configuration); ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready'); // await initialization and signal this back to electron-main - await main(server, data, configuration); + await sharedProcess.open(); ipcRenderer.send('vscode:shared-process->electron-main=init-done'); } diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 866e57d2ad..7df7b858a2 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -12,8 +12,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top - const perf = bootstrapWindow.perfLib(); - perf.mark('renderer/started'); + performance.mark('code/didStartRenderer'); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -25,10 +24,10 @@ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - async function (workbench, configuration) { + function (_, configuration) { // Mark start of workbench - perf.mark('didLoadWorkbenchMain'); + performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore return require('vs/workbench/electron-browser/desktop.main').main(configuration); @@ -42,19 +41,34 @@ loaderConfig.recordStats = true; }, beforeRequire: function () { - perf.mark('willLoadWorkbenchMain'); + performance.mark('code/willLoadWorkbenchMain'); } } ); + // add default trustedTypes-policy for logging and to workaround + // lib/platform limitations + window.trustedTypes?.createPolicy('default', { + createHTML(value) { + // see https://github.com/electron/electron/issues/27211 + // Electron webviews use a static innerHTML default value and + // that isn't trusted. We use a default policy to check for the + // exact value of that innerHTML-string and only allow that. + if (value === '') { + return value; + } + throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // return value; + } + }); //region Helpers /** * @returns {{ - * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, - * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'), - * perfLib: () => { mark: (name: string) => void } + * load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') * }} */ function bootstrapWindowLib() { @@ -68,12 +82,11 @@ * colorScheme: ('light' | 'dark' | 'hc'), * autoDetectHighContrast?: boolean, * extensionDevelopmentPath?: string[], - * folderUri?: object, - * workspace?: object + * workspace?: import('../../../platform/workspaces/common/workspaces').IWorkspaceIdentifier | import('../../../platform/workspaces/common/workspaces').ISingleFolderWorkspaceIdentifier * }} configuration */ function showPartsSplash(configuration) { - perf.mark('willShowPartsSplash'); + performance.mark('code/willShowPartsSplash'); let data; if (typeof configuration.partsSplashPath === 'string') { @@ -148,7 +161,7 @@ splash.appendChild(activityDiv); // part: side bar (only when opening workspace/folder) - if (configuration.folderUri || configuration.workspace) { + if (configuration.workspace) { // folder or workspace -> status bar color, sidebar const sideDiv = document.createElement('div'); sideDiv.setAttribute('style', `position: absolute; height: calc(100% - ${layoutInfo.titleBarHeight}px); top: ${layoutInfo.titleBarHeight}px; ${layoutInfo.sideBarSide}: ${layoutInfo.activityBarWidth}px; width: ${layoutInfo.sideBarWidth}px; background-color: ${colorInfo.sideBarBackground};`); @@ -157,13 +170,13 @@ // part: statusbar const statusDiv = document.createElement('div'); - statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.folderUri || configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); + statusDiv.setAttribute('style', `position: absolute; width: 100%; bottom: 0; left: 0; height: ${layoutInfo.statusBarHeight}px; background-color: ${configuration.workspace ? colorInfo.statusBarBackground : colorInfo.statusBarNoFolderBackground};`); splash.appendChild(statusDiv); document.body.appendChild(splash); } - perf.mark('didShowPartsSplash'); + performance.mark('code/didShowPartsSplash'); } //#endregion diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 1299e0f105..936bcf9587 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session } from 'electron'; -import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; +import { release } from 'os'; +import { IProcessEnvironment, isWindows, isMacintosh, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { resolveShellEnv } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/electron-main/updateIpc'; -import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { Server, connect } from 'vs/base/parts/ipc/node/ipc.net'; +import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron'; +import { Server as NodeIPCServer } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; import { LaunchMainService, ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -28,19 +29,17 @@ import { IOpenURLOptions, IURLService } from 'vs/platform/url/common/url'; import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc'; import { ITelemetryService, machineIdKey } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; -import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2'; import { FileProtocolHandler } from 'vs/code/electron-main/protocol'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService'; +import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; @@ -64,14 +63,13 @@ import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainSe import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { NativeURLService } from 'vs/platform/url/common/urlService'; -import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WorkspacesManagementMainService, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { statSync } from 'fs'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { INativeHostMainService, NativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; -import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; @@ -81,7 +79,7 @@ import { stripComments } from 'vs/base/common/json'; import { generateUuid } from 'vs/base/common/uuid'; import { VSBuffer } from 'vs/base/common/buffer'; import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { ActiveWindowManager } from 'vs/platform/windows/common/windowTracker'; +import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { IKeyboardLayoutMainService, KeyboardLayoutMainService } from 'vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { DisplayMainService, IDisplayMainService } from 'vs/platform/display/electron-main/displayMainService'; @@ -90,6 +88,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; +import { once } from 'vs/base/common/functional'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -97,7 +96,7 @@ export class CodeApplication extends Disposable { private nativeHostMainService: INativeHostMainService | undefined; constructor( - private readonly mainIpcServer: Server, + private readonly mainIpcServer: NodeIPCServer, private readonly userEnv: IProcessEnvironment, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService private readonly logService: ILogService, @@ -150,19 +149,19 @@ export class CodeApplication extends Disposable { event.preventDefault(); }); app.on('remote-get-global', (event, sender, module) => { - this.logService.trace(`App#on(remote-get-global): prevented on ${module}`); + this.logService.trace(`app#on(remote-get-global): prevented on ${module}`); event.preventDefault(); }); app.on('remote-get-builtin', (event, sender, module) => { - this.logService.trace(`App#on(remote-get-builtin): prevented on ${module}`); + this.logService.trace(`app#on(remote-get-builtin): prevented on ${module}`); if (module !== 'clipboard') { event.preventDefault(); } }); app.on('remote-get-current-window', event => { - this.logService.trace(`App#on(remote-get-current-window): prevented`); + this.logService.trace(`app#on(remote-get-current-window): prevented`); event.preventDefault(); }); @@ -171,7 +170,7 @@ export class CodeApplication extends Disposable { return; // the driver needs access to web contents } - this.logService.trace(`App#on(remote-get-current-web-contents): prevented`); + this.logService.trace(`app#on(remote-get-current-web-contents): prevented`); event.preventDefault(); }); @@ -271,9 +270,13 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers + let slowShellResolveWarningShown = false; ipcMain.on('vscode:fetchShellEnv', async event => { + + // DO NOT remove: not only usual windows are fetching the + // shell environment but also shared process, issue reporter + // etc, so we need to reply via `webContents` always const webContents = event.sender; - const window = this.windowsMainService?.getWindowByWebContents(event.sender); let replied = false; @@ -291,11 +294,19 @@ export class CodeApplication extends Disposable { } // Handle slow shell environment resolve calls: - // - a warning after 3s but continue to resolve - // - an error after 10s and stop trying to resolve + // - a warning after 3s but continue to resolve (only once in active window) + // - an error after 10s and stop trying to resolve (in every window where this happens) const cts = new CancellationTokenSource(); - const shellEnvSlowWarningHandle = setTimeout(() => window?.sendWhenReady('vscode:showShellEnvSlowWarning', cts.token), 3000); - const shellEnvTimeoutErrorHandle = setTimeout(function () { + + const shellEnvSlowWarningHandle = setTimeout(() => { + if (!slowShellResolveWarningShown) { + this.windowsMainService?.sendToFocused('vscode:showShellEnvSlowWarning', cts.token); + slowShellResolveWarningShown = true; + } + }, 3000); + + const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process!! + const shellEnvTimeoutErrorHandle = setTimeout(() => { cts.dispose(true); window?.sendWhenReady('vscode:showShellEnvTimeoutError', CancellationToken.None); acceptShellEnv({}); @@ -304,8 +315,11 @@ export class CodeApplication extends Disposable { // Prefer to use the args and env from the target window // when resolving the shell env. It is possible that // a first window was opened from the UI but a second - // from the CLI and that has implications for wether to + // from the CLI and that has implications for whether to // resolve the shell environment or not. + // + // Window can be undefined for e.g. the shared process + // that is not part of our windows registry! let args: NativeParsedArgs; let env: NodeJS.ProcessEnv; if (window?.config) { @@ -321,10 +335,10 @@ export class CodeApplication extends Disposable { acceptShellEnv(shellEnv); }); - ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { + ipcMain.handle('vscode:writeNlsFile', async (event, path: unknown, data: unknown) => { const uri = this.validateNlsPath([path]); if (!uri || typeof data !== 'string') { - return Promise.reject('Invalid operation (vscode:writeNlsFile)'); + throw new Error('Invalid operation (vscode:writeNlsFile)'); } return this.fileService.writeFile(uri, VSBuffer.fromString(data)); @@ -333,7 +347,7 @@ export class CodeApplication extends Disposable { ipcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { const uri = this.validateNlsPath(paths); if (!uri) { - return Promise.reject('Invalid operation (vscode:readNlsFile)'); + throw new Error('Invalid operation (vscode:readNlsFile)'); } return (await this.fileService.readFile(uri)).value.toString(); @@ -427,16 +441,20 @@ export class CodeApplication extends Disposable { // Spawn shared process after the first window has opened and 3s have passed const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); - const sharedProcessClient = sharedProcess.whenIpcReady().then(() => { - this.logService.trace('Shared process: IPC ready'); + const sharedProcessClient = (async () => { + this.logService.trace('Main->SharedProcess#connect'); - return connect(this.environmentService.sharedIPCHandle, 'main'); - }); - const sharedProcessReady = sharedProcess.whenReady().then(() => { - this.logService.trace('Shared process: init ready'); + const port = await sharedProcess.connect(); + + this.logService.trace('Main->SharedProcess#connect: connection established'); + + return new MessagePortClient(port, 'main'); + })(); + const sharedProcessReady = (async () => { + await sharedProcess.whenReady(); return sharedProcessClient; - }); + })(); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { sharedProcess.spawn(await resolveShellEnv(this.logService, this.environmentService.args, process.env)); @@ -454,12 +472,8 @@ export class CodeApplication extends Disposable { this._register(server); } - // Setup Auth Handler (TODO@ben remove old auth handler eventually) - if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') === false) { - this._register(new ProxyAuthHandler()); - } else { - this._register(appInstantiationService.createInstance(ProxyAuthHandler2)); - } + // Setup Auth Handler + this._register(appInstantiationService.createInstance(ProxyAuthHandler)); // {{SQL CARBON EDIT}} Cast here to avoid compilation error (not finding constructor?) // Open Windows const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient, fileProtocolHandler)); @@ -487,7 +501,7 @@ export class CodeApplication extends Disposable { return machineId; } - private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise>): Promise { + private async createServices(machineId: string, sharedProcess: SharedProcess, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); switch (process.platform) { @@ -496,8 +510,8 @@ export class CodeApplication extends Disposable { break; case 'linux': - if (process.env.SNAP && process.env.SNAP_REVISION) { - services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env.SNAP, process.env.SNAP_REVISION])); + if (isLinuxSnap) { + services.set(IUpdateService, new SyncDescriptor(SnapUpdateService, [process.env['SNAP'], process.env['SNAP_REVISION']])); } else { services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); } @@ -510,7 +524,6 @@ export class CodeApplication extends Disposable { services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, this.userEnv])); services.set(IDialogMainService, new SyncDescriptor(DialogMainService)); - services.set(ISharedProcessMainService, new SyncDescriptor(SharedProcessMainService, [sharedProcess])); services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService)); services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics'))))); @@ -518,9 +531,9 @@ export class CodeApplication extends Disposable { services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId])); services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService)); services.set(IDisplayMainService, new SyncDescriptor(DisplayMainService)); - services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService)); + services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess])); services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); - services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService)); + services.set(IWorkspacesService, new SyncDescriptor(WorkspacesMainService)); services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService)); services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService)); @@ -533,13 +546,13 @@ export class CodeApplication extends Disposable { services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService)); services.set(IURLService, new SyncDescriptor(NativeURLService)); - services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); + services.set(IWorkspacesManagementMainService, new SyncDescriptor(WorkspacesManagementMainService)); // Telemetry if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(product.commit, product.version, machineId, product.msftInternalDomains, this.environmentService.installSourcePath); + 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 config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; @@ -589,7 +602,7 @@ export class CodeApplication extends Disposable { }); } - private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { + private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] { // Register more Main IPC services const launchMainService = accessor.get(ILaunchMainService); @@ -622,10 +635,6 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('nativeHost', nativeHostChannel); sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel)); - const sharedProcessMainService = accessor.get(ISharedProcessMainService); - const sharedProcessChannel = createChannelReceiver(sharedProcessMainService); - electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel); - const workspacesService = accessor.get(IWorkspacesService); const workspacesChannel = createChannelReceiver(workspacesService); electronIpcServer.registerChannel('workspaces', workspacesChannel); @@ -705,6 +714,8 @@ export class CodeApplication extends Disposable { }); // Create a URL handler to open file URIs in the active window + // or open new windows. The URL handler will be invoked from + // protocol invocations outside of VSCode. const app = this; const environmentService = this.environmentService; urlService.registerHandler({ @@ -718,13 +729,15 @@ export class CodeApplication extends Disposable { // Check for URIs to open in window const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); if (windowOpenableFromProtocolLink) { - windowsMainService.open({ + const [window] = windowsMainService.open({ context: OpenContext.API, cli: { ...environmentService.args }, urisToOpen: [windowOpenableFromProtocolLink], gotoLineMode: true }); + window.focus(); // this should help ensuring that the right window gets focus when multiple are opened + return true; } @@ -832,7 +845,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), 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.environmentService), 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 }); @@ -901,8 +914,25 @@ export class CodeApplication extends Disposable { // Signal phase: after window open this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; + // Windows: install mutex + const win32MutexName = product.win32MutexName; + if (isWindows && win32MutexName) { + try { + const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; + const mutex = new WindowsMutex(win32MutexName); + once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); + } catch (error) { + this.logService.error(error); + } + } + // Remote Authorities - this.handleRemoteAuthorities(); + protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { + callback({ + url: request.url.replace(/^vscode-remote-resource:/, 'http:'), + method: request.method + }); + }); // Initialize update service const updateService = accessor.get(IUpdateService); @@ -934,19 +964,11 @@ 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)); } } catch (error) { this.logService.error(error); } } - - private handleRemoteAuthorities(): void { - protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { - callback({ - url: request.url.replace(/^vscode-remote-resource:/, 'http:'), - method: request.method - }); - }); - } } diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index 78f1a86038..a65335c7d8 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -3,18 +3,28 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; -import { FileAccess } from 'vs/base/common/network'; -import { BrowserWindow, BrowserWindowConstructorOptions, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron'; +import { hash } from 'vs/base/common/hash'; +import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; +import { generateUuid } from 'vs/base/common/uuid'; +import product from 'vs/platform/product/common/product'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { + firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 +} type LoginEvent = { event: ElectronEvent; - webContents: WebContents; - req: Request; authInfo: AuthInfo; - cb: (username: string, password: string) => void; + req: ElectronAuthenticationResponseDetails; + + callback: (username?: string, password?: string) => void; }; type Credentials = { @@ -22,81 +32,211 @@ type Credentials = { password: string; }; +enum ProxyAuthState { + + /** + * Initial state: we will try to use stored credentials + * first to reply to the auth challenge. + */ + Initial = 1, + + /** + * We used stored credentials and are still challenged, + * so we will show a login dialog next. + */ + StoredCredentialsUsed, + + /** + * Finally, if we showed a login dialog already, we will + * not show any more login dialogs until restart to reduce + * the UI noise. + */ + LoginDialogShown +} + export class ProxyAuthHandler extends Disposable { - declare readonly _serviceBrand: undefined; + private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - private retryCount = 0; + private pendingProxyResolve: Promise | undefined = undefined; - constructor() { + private state = ProxyAuthState.Initial; + + private sessionCredentials: Credentials | undefined = undefined; + + constructor( + @ILogService private readonly logService: ILogService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService + ) { super(); this.registerListeners(); } private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb })); + const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); this._register(onLogin(this.onLogin, this)); } - private onLogin({ event, authInfo, cb }: LoginEvent): void { + private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { if (!authInfo.isProxy) { - return; + return; // only for proxy } - if (this.retryCount++ > 1) { - return; + if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { + this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); + + return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) } + // Signal we handle this event on our own, otherwise + // Electron will ignore our provided credentials. event.preventDefault(); - const opts: BrowserWindowConstructorOptions = { - alwaysOnTop: true, - skipTaskbar: true, - resizable: false, - width: 450, - height: 225, - show: true, - title: 'VS Code', - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - sandbox: true, - contextIsolation: true, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - devTools: false - } - }; + let credentials: Credentials | undefined = undefined; + if (!this.pendingProxyResolve) { + this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - opts.parent = focusedWindow; - opts.modal = true; + this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); + try { + credentials = await this.pendingProxyResolve; + } finally { + this.pendingProxyResolve = undefined; + } + } else { + this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); + + credentials = await this.pendingProxyResolve; } - const win = new BrowserWindow(opts); - const windowUrl = FileAccess.asBrowserUri('vs/code/electron-sandbox/proxy/auth.html', require); - const proxyUrl = `${authInfo.host}:${authInfo.port}`; - const title = localize('authRequire', "Proxy Authentication Required"); - const message = localize('proxyauth', "The proxy {0} requires authentication.", proxyUrl); + // According to Electron docs, it is fine to call back without + // username or password to signal that the authentication was handled + // by us, even though without having credentials received: + // + // > If `callback` is called without a username or password, the authentication + // > request will be cancelled and the authentication error will be returned to the + // > page. + callback(credentials?.username, credentials?.password); + } - const onWindowClose = () => cb('', ''); - win.on('close', onWindowClose); + private async resolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - win.setMenu(null); - win.webContents.on('did-finish-load', () => { - const data = { title, message }; - win.webContents.send('vscode:openProxyAuthDialog', data); - }); - win.webContents.on('ipc-message', (event, channel, credentials: Credentials) => { - if (channel === 'vscode:proxyAuthResponse') { - const { username, password } = credentials; - cb(username, password); - win.removeListener('close', onWindowClose); - win.close(); + try { + const credentials = await this.doResolveProxyCredentials(authInfo); + if (credentials) { + this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); + + return credentials; + } else { + this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); } + } finally { + this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); + } + + return undefined; + } + + private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); + + // Compute a hash over the authentication info to be used + // with the credentials store to return the right credentials + // given the properties of the auth request + // (see https://github.com/microsoft/vscode/issues/109497) + const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); + + // Find any previously stored credentials + let storedUsername: string | undefined = undefined; + let storedPassword: string | undefined = undefined; + try { + const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + if (encryptedSerializedProxyCredentials) { + const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); + + storedUsername = credentials.username; + storedPassword = credentials.password; + } + } catch (error) { + this.logService.error(error); // handle errors by asking user for login via dialog + } + + // Reply with stored credentials unless we used them already. + // In that case we need to show a login dialog again because + // they seem invalid. + if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); + this.state = ProxyAuthState.StoredCredentialsUsed; + + return { username: storedUsername, password: storedPassword }; + } + + // Find suitable window to show dialog: prefer to show it in the + // active window because any other network request will wait on + // the credentials and we want the user to present the dialog. + const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + if (!window) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); + + return undefined; // unexpected + } + + this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); + + // Open proxy dialog + const payload = { + authInfo, + username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored + password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored + replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` + }; + window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); + this.state = ProxyAuthState.LoginDialogShown; + + // Handle reply + const loginDialogCredentials = await new Promise(resolve => { + 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); + + // We got credentials from the window + if (reply) { + const credentials: Credentials = { username: reply.username, password: reply.password }; + + // Update stored credentials based on `remember` flag + try { + if (reply.remember) { + const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); + await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); + } else { + await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + } + } catch (error) { + this.logService.error(error); // handle gracefully + } + + resolve({ username: credentials.username, password: credentials.password }); + } + + // We did not get any credentials from the window (e.g. cancelled) + else { + resolve(undefined); + } + } + }; + + window.win.webContents.on('ipc-message', proxyAuthResponseHandler); }); - win.loadURL(windowUrl.toString(true)); + + // Remember credentials for the session in case + // the credentials are wrong and we show the dialog + // again + this.sessionCredentials = loginDialogCredentials; + + return loginDialogCredentials; } } diff --git a/src/vs/code/electron-main/auth2.ts b/src/vs/code/electron-main/auth2.ts deleted file mode 100644 index a27e55d021..0000000000 --- a/src/vs/code/electron-main/auth2.ts +++ /dev/null @@ -1,242 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { generateUuid } from 'vs/base/common/uuid'; -import product from 'vs/platform/product/common/product'; -import { CancellationToken } from 'vs/base/common/cancellation'; - -interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { - firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 -} - -type LoginEvent = { - event: ElectronEvent; - authInfo: AuthInfo; - req: ElectronAuthenticationResponseDetails; - - callback: (username?: string, password?: string) => void; -}; - -type Credentials = { - username: string; - password: string; -}; - -enum ProxyAuthState { - - /** - * Initial state: we will try to use stored credentials - * first to reply to the auth challenge. - */ - Initial = 1, - - /** - * We used stored credentials and are still challenged, - * so we will show a login dialog next. - */ - StoredCredentialsUsed, - - /** - * Finally, if we showed a login dialog already, we will - * not show any more login dialogs until restart to reduce - * the UI noise. - */ - LoginDialogShown -} - -export class ProxyAuthHandler2 extends Disposable { - - private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - - private pendingProxyResolve: Promise | undefined = undefined; - - private state = ProxyAuthState.Initial; - - private sessionCredentials: Credentials | undefined = undefined; - - constructor( - @ILogService private readonly logService: ILogService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); - this._register(onLogin(this.onLogin, this)); - } - - private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { - if (!authInfo.isProxy) { - return; // only for proxy - } - - if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { - this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); - - return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) - } - - // Signal we handle this event on our own, otherwise - // Electron will ignore our provided credentials. - event.preventDefault(); - - let credentials: Credentials | undefined = undefined; - if (!this.pendingProxyResolve) { - this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - - this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); - try { - credentials = await this.pendingProxyResolve; - } finally { - this.pendingProxyResolve = undefined; - } - } else { - this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); - - credentials = await this.pendingProxyResolve; - } - - // According to Electron docs, it is fine to call back without - // username or password to signal that the authentication was handled - // by us, even though without having credentials received: - // - // > If `callback` is called without a username or password, the authentication - // > request will be cancelled and the authentication error will be returned to the - // > page. - callback(credentials?.username, credentials?.password); - } - - private async resolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - - try { - const credentials = await this.doResolveProxyCredentials(authInfo); - if (credentials) { - this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); - - return credentials; - } else { - this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); - } - } finally { - this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); - } - - return undefined; - } - - private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); - - // Compute a hash over the authentication info to be used - // with the credentials store to return the right credentials - // given the properties of the auth request - // (see https://github.com/microsoft/vscode/issues/109497) - const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); - - // Find any previously stored credentials - let storedUsername: string | undefined = undefined; - let storedPassword: string | undefined = undefined; - try { - const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - if (encryptedSerializedProxyCredentials) { - const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); - - storedUsername = credentials.username; - storedPassword = credentials.password; - } - } catch (error) { - this.logService.error(error); // handle errors by asking user for login via dialog - } - - // Reply with stored credentials unless we used them already. - // In that case we need to show a login dialog again because - // they seem invalid. - if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); - this.state = ProxyAuthState.StoredCredentialsUsed; - - return { username: storedUsername, password: storedPassword }; - } - - // Find suitable window to show dialog: prefer to show it in the - // active window because any other network request will wait on - // the credentials and we want the user to present the dialog. - const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); - if (!window) { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); - - return undefined; // unexpected - } - - this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); - - // Open proxy dialog - const payload = { - authInfo, - username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored - password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored - replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` - }; - window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); - this.state = ProxyAuthState.LoginDialogShown; - - // Handle reply - const loginDialogCredentials = await new Promise(resolve => { - 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); - - // We got credentials from the window - if (reply) { - const credentials: Credentials = { username: reply.username, password: reply.password }; - - // Update stored credentials based on `remember` flag - try { - if (reply.remember) { - const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); - await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); - } else { - await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - } - } catch (error) { - this.logService.error(error); // handle gracefully - } - - resolve({ username: credentials.username, password: credentials.password }); - } - - // We did not get any credentials from the window (e.g. cancelled) - else { - resolve(undefined); - } - } - }; - - window.win.webContents.on('ipc-message', proxyAuthResponseHandler); - }); - - // Remember credentials for the session in case - // the credentials are wrong and we show the dialog - // again - this.sessionCredentials = loginDialogCredentials; - - return loginDialogCredentials; - } -} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 623fe9c819..394ebe673f 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,15 +5,16 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import * as fs from 'fs'; +import { promises, unlinkSync } from 'fs'; +import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; -import { mkdirp } from 'vs/base/node/pfs'; import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { Server, serve, connect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; +import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net'; +import { Client as NodeIPCClient } from 'vs/base/parts/ipc/common/ipc.net'; import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; @@ -29,17 +30,15 @@ import { ConfigurationService } from 'vs/platform/configuration/common/configura import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import { CodeApplication } from 'vs/code/electron-main/app'; -import { localize } from 'vs/nls'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { once } from 'vs/base/common/functional'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; +import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; @@ -53,6 +52,8 @@ import { rtrim, trim } from 'vs/base/common/strings'; import { basename, resolve } from 'vs/base/common/path'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; class ExpectedError extends Error { readonly isExpected = true; @@ -184,7 +185,7 @@ class CodeMain { environmentService.globalStorageHome.fsPath, environmentService.workspaceStorageHome.fsPath, environmentService.backupHome - ].map((path): undefined | Promise => path ? mkdirp(path) : undefined)); + ].map((path): undefined | Promise => path ? promises.mkdir(path, { recursive: true }) : undefined)); // Configuration service const configurationServiceInitialization = configurationService.initialize(); @@ -212,14 +213,14 @@ class CodeMain { return instanceEnvironment; } - private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, retry: boolean): Promise { + private async doStartup(args: NativeParsedArgs, logService: ILogService, environmentService: 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 server: Server; + let server: NodeIPCServer; try { - server = await serve(environmentService.mainIPCHandle); + server = await nodeIPCServe(environmentService.mainIPCHandle); once(lifecycleMainService.onWillShutdown)(() => server.dispose()); } catch (error) { @@ -235,9 +236,9 @@ class CodeMain { } // there's a running instance, let's connect to it - let client: Client; + let client: NodeIPCClient; try { - client = await connect(environmentService.mainIPCHandle, 'main'); + client = await nodeIPCConnect(environmentService.mainIPCHandle, 'main'); } catch (error) { // Handle unexpected connection errors by showing a dialog to the user @@ -256,7 +257,7 @@ class CodeMain { // let's delete it, since we can't connect to it and then // retry the whole thing try { - fs.unlinkSync(environmentService.mainIPCHandle); + unlinkSync(environmentService.mainIPCHandle); } catch (error) { logService.warn('Could not delete obsolete instance handle', error); @@ -293,11 +294,7 @@ class CodeMain { // Process Info if (args.status) { return instantiationService.invokeFunction(async () => { - - // Create a diagnostic service connected to the existing shared process - const sharedProcessClient = await connect(environmentService.sharedIPCHandle, 'main'); - const diagnosticsChannel = sharedProcessClient.getChannel('diagnostics'); - const diagnosticsService = createChannelSender(diagnosticsChannel); + const diagnosticsService = new DiagnosticsService(NullTelemetryService); const mainProcessInfo = await launchService.getMainProcessInfo(); const remoteDiagnostics = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); const diagnostics = await diagnosticsService.getDiagnostics(mainProcessInfo, remoteDiagnostics); @@ -343,11 +340,11 @@ class CodeMain { private handleStartupDataDirError(environmentService: IEnvironmentMainService, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { - const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]); + const directories = coalesce([environmentService.userDataPath, environmentService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentService)); this.showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), - localize('startupUserDataAndExtensionsDirErrorDetail', "Please make sure the following directories are writeable:\n\n{0}", directories.join('\n')) + localize('startupUserDataAndExtensionsDirErrorDetail', "{0}\n\nPlease make sure the following directories are writeable:\n\n{1}", toErrorMessage(error), directories.join('\n')) ); } } diff --git a/src/vs/code/electron-main/protocol.ts b/src/vs/code/electron-main/protocol.ts index dc25c8a6f9..155776670b 100644 --- a/src/vs/code/electron-main/protocol.ts +++ b/src/vs/code/electron-main/protocol.ts @@ -11,7 +11,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { session } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { TernarySearchTree } from 'vs/base/common/map'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isPreferringBrowserCodeLoad } from 'vs/base/common/platform'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; type ProtocolCallback = { (result: string | Electron.FilePathWithHeaders | { error: number }): void }; @@ -37,15 +37,17 @@ export class FileProtocolHandler extends Disposable { // Register vscode-file:// handler defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback)); - // Block any file:// access (sandbox only) - if (environmentService.args.__sandbox) { + // Block any file:// access (explicitly enabled only) + if (isPreferringBrowserCodeLoad) { + this.logService.info(`Intercepting ${Schemas.file}: protocol and blocking it`); + defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback)); } // Cleanup this._register(toDisposable(() => { defaultSession.protocol.unregisterProtocol(Schemas.vscodeFileResource); - if (environmentService.args.__sandbox) { + if (isPreferringBrowserCodeLoad) { defaultSession.protocol.uninterceptProtocol(Schemas.file); } })); diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index 9eee0e2e7d..a535d731f9 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -3,25 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { memoize } from 'vs/base/common/decorators'; +import { BrowserWindow, ipcMain, Event, MessagePortMain } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { BrowserWindow, ipcMain, WebContents, Event as ElectronEvent } from 'electron'; -import { ISharedProcess } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; import { Barrier } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; +import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform'; +import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp'; +import { assertIsDefined } from 'vs/base/common/types'; -export class SharedProcess implements ISharedProcess { +export class SharedProcess extends Disposable implements ISharedProcess { - private barrier = new Barrier(); + private readonly whenSpawnedBarrier = new Barrier(); - private window: BrowserWindow | null = null; - - private readonly _whenReady: Promise; + private window: BrowserWindow | undefined = undefined; + private windowCloseListener: ((event: Event) => void) | undefined = undefined; constructor( private readonly machineId: string, @@ -31,17 +31,125 @@ export class SharedProcess implements ISharedProcess { @ILogService private readonly logService: ILogService, @IThemeMainService private readonly themeMainService: IThemeMainService ) { - // overall ready promise when shared process signals initialization is done - this._whenReady = new Promise(c => ipcMain.once('vscode:shared-process->electron-main=init-done', () => c(undefined))); + super(); + + this.registerListeners(); } - @memoize - private get _whenIpcReady(): Promise { + private registerListeners(): void { + + // Lifecycle + this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown())); + + // Shared process connections from workbench windows + ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => { + this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel'); + + // await the shared process to be overall ready + // we do not just wait for IPC ready because the + // workbench window will communicate directly + await this.whenReady(); + + // connect to the shared process window + const port = await this.connect(); + + // Check back if the requesting window meanwhile closed + // Since shared process is delayed on startup there is + // a chance that the window close before the shared process + // was ready for a connection. + if (e.sender.isDestroyed()) { + return port.close(); + } + + // send the port back to the requesting window + e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]); + }); + } + + private onWillShutdown(): void { + const window = this.window; + if (!window) { + return; // possibly too early before created + } + + // Signal exit to shared process when shutting down + if (!window.isDestroyed() && !window.webContents.isDestroyed()) { + window.webContents.send('vscode:electron-main->shared-process=exit'); + } + + // Shut the shared process down when we are quitting + // + // Note: because we veto the window close, we must first remove our veto. + // Otherwise the application would never quit because the shared process + // window is refusing to close! + // + if (this.windowCloseListener) { + window.removeListener('close', this.windowCloseListener); + this.windowCloseListener = undefined; + } + + // Electron seems to crash on Windows without this setTimeout :| + setTimeout(() => { + try { + window.close(); + } catch (err) { + // ignore, as electron is already shutting down + } + + this.window = undefined; + }, 0); + } + + private _whenReady: Promise | undefined = undefined; + whenReady(): Promise { + if (!this._whenReady) { + // Overall signal that the shared process window was loaded and + // all services within have been created. + this._whenReady = new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => { + this.logService.trace('SharedProcess: Overall ready'); + + resolve(); + })); + } + + return this._whenReady; + } + + private _whenIpcReady: Promise | undefined = undefined; + private get whenIpcReady() { + if (!this._whenIpcReady) { + this._whenIpcReady = (async () => { + + // Always wait for `spawn()` + await this.whenSpawnedBarrier.wait(); + + // Create window for shared process + this.createWindow(); + + // Listeners + this.registerWindowListeners(); + + // Wait for window indicating that IPC connections are accepted + await new Promise(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { + this.logService.trace('SharedProcess: IPC ready'); + + resolve(); + })); + })(); + } + + return this._whenIpcReady; + } + + private createWindow(): void { + + // shared process is a hidden window by default this.window = new BrowserWindow({ show: false, backgroundColor: this.themeMainService.getBackgroundColor(), webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: browserCodeLoadingCacheStrategy, nodeIntegration: true, enableWebSQL: false, enableRemoteModule: false, @@ -52,118 +160,85 @@ export class SharedProcess implements ISharedProcess { disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); - const config = { - appRoot: this.environmentService.appRoot, + + const config: ISharedProcessConfiguration = { machineId: this.machineId, + windowId: this.window.id, + appRoot: this.environmentService.appRoot, nodeCachedDataDir: this.environmentService.nodeCachedDataDir, + backupWorkspacesPath: this.environmentService.backupWorkspacesPath, userEnv: this.userEnv, - windowId: this.window.id + sharedIPCHandle: this.environmentService.sharedIPCHandle, + args: this.environmentService.args, + logLevel: this.logService.getLevel() }; - const windowUrl = (this.environmentService.sandbox ? - FileAccess._asCodeFileUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) : - FileAccess.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)) - .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }); - this.window.loadURL(windowUrl.toString(true)); + // Load with config + this.window.loadURL(FileAccess + .asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) + .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) + .toString(true) + ); + } - // Prevent the window from dying - const onClose = (e: ElectronEvent) => { + private registerWindowListeners(): void { + if (!this.window) { + return; + } + + // Prevent the window from closing + this.windowCloseListener = (e: Event) => { this.logService.trace('SharedProcess#close prevented'); // We never allow to close the shared process unless we get explicitly disposed() e.preventDefault(); // Still hide the window though if visible - if (this.window && this.window.isVisible()) { + if (this.window?.isVisible()) { this.window.hide(); } }; - this.window.on('close', onClose); + this.window.on('close', this.windowCloseListener); - const disposables = new DisposableStore(); - - this.lifecycleMainService.onWillShutdown(() => { - disposables.dispose(); - - // Shut the shared process down when we are quitting - // - // Note: because we veto the window close, we must first remove our veto. - // Otherwise the application would never quit because the shared process - // window is refusing to close! - // - if (this.window) { - this.window.removeListener('close', onClose); - } - - // Electron seems to crash on Windows without this setTimeout :| - setTimeout(() => { - try { - if (this.window) { - this.window.close(); - } - } catch (err) { - // ignore, as electron is already shutting down - } - - this.window = null; - }, 0); - }); - - return new Promise(c => { - // send payload once shared process is ready to receive it - disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => { - sender.send('vscode:electron-main->shared-process=payload', { - sharedIPCHandle: this.environmentService.sharedIPCHandle, - args: this.environmentService.args, - logLevel: this.logService.getLevel(), - backupWorkspacesPath: this.environmentService.backupWorkspacesPath, - nodeCachedDataDir: this.environmentService.nodeCachedDataDir - }); - - // signal exit to shared process when we get disposed - disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit'))); - - // complete IPC-ready promise when shared process signals this to us - ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined)); - })); - }); + // Crashes & Unrsponsive & Failed to load + this.window.webContents.on('render-process-gone', (event, details) => this.logService.error(`SharedProcess: crashed (detail: ${details?.reason})`)); + this.window.on('unresponsive', () => this.logService.error('SharedProcess: detected unresponsive window')); + this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('SharedProcess: failed to load window, ', errorDescription)); } spawn(userEnv: NodeJS.ProcessEnv): void { this.userEnv = { ...this.userEnv, ...userEnv }; - this.barrier.open(); + + // Release barrier + this.whenSpawnedBarrier.open(); } - async whenReady(): Promise { - await this.barrier.wait(); - await this._whenReady; + async connect(): Promise { + + // Wait for shared process being ready to accept connection + await this.whenIpcReady; + + // Connect and return message port + const window = assertIsDefined(this.window); + return connectMessagePort(window); } - async whenIpcReady(): Promise { - await this.barrier.wait(); - await this._whenIpcReady; - } + async toggle(): Promise { - toggle(): void { - if (!this.window || this.window.isVisible()) { - this.hide(); - } else { - this.show(); + // wait for window to be created + await this.whenIpcReady; + + if (!this.window) { + return; // possibly disposed already } - } - show(): void { - if (this.window) { + if (this.window.isVisible()) { + this.window.webContents.closeDevTools(); + this.window.hide(); + } else { this.window.show(); this.window.webContents.openDevTools(); } } - - hide(): void { - if (this.window) { - this.window.webContents.closeDevTools(); - this.window.hide(); - } - } } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 2cac510ec7..9b4c8f60b9 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as nls from 'vs/nls'; -import * as perf from 'vs/base/common/performance'; +import { release } from 'os'; +import { join } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; +import { getMarks, mark } from 'vs/base/common/performance'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, Details } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -18,17 +18,17 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; 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 { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { browserCodeLoadingCacheStrategy, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +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'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -102,32 +102,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { private hiddenTitleBarStyle: boolean | undefined; private showTimeoutHandle: NodeJS.Timeout | undefined; - private _lastFocusTime: number; - private _readyState: ReadyState; + private _lastFocusTime = -1; + private _readyState = ReadyState.NONE; private windowState: IWindowState; private currentMenuBarVisibility: MenuBarVisibility | undefined; private representedFilename: string | undefined; private documentEdited: boolean | undefined; - private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[]; + private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; private marketplaceHeadersPromise: Promise; - private readonly touchBarGroups: TouchBarSegmentedControl[]; + private readonly touchBarGroups: TouchBarSegmentedControl[] = []; - private currentHttpProxy?: string; - private currentNoProxy?: string; + private currentHttpProxy: string | undefined = undefined; + private currentNoProxy: string | undefined = undefined; constructor( config: IWindowCreationOptions, @ILogService private readonly logService: ILogService, @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, - @IStorageMainService private readonly storageService: IStorageMainService, + @IStorageMainService storageService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @@ -135,11 +135,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { ) { super(); - this.touchBarGroups = []; - this._lastFocusTime = -1; - this._readyState = ReadyState.NONE; - this.whenReadyCallbacks = []; - //#region create browser window { // Load window state @@ -164,6 +159,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { title: product.nameLong, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: browserCodeLoadingCacheStrategy, enableWebSQL: false, enableRemoteModule: false, spellcheck: false, @@ -185,13 +181,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { } }; + if (browserCodeLoadingCacheStrategy) { + this.logService.info(`window#ctor: using vscode-file protocol and V8 cache options: ${browserCodeLoadingCacheStrategy}`); + } + // Apply icon to window // Linux: always // Windows: only when running out of sources, otherwise an icon is set by us on the executable if (isLinux) { - options.icon = path.join(this.environmentService.appRoot, 'resources/linux/code.png'); + options.icon = join(this.environmentService.appRoot, 'resources/linux/code.png'); } else if (isWindows && !this.environmentService.isBuilt) { - options.icon = path.join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); + options.icon = join(this.environmentService.appRoot, 'resources/win32/code_150x150.png'); } if (isMacintosh && !this.useNativeFullScreen()) { @@ -233,7 +233,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any } - // TODO@Ben (Electron 4 regression): when running on multiple displays where the target display + // TODO@bpasero (Electron 4 regression): when running on multiple displays where the target display // to open the window has a larger resolution than the primary display, the window will not size // correctly unless we set the bounds again (https://github.com/microsoft/vscode/issues/74872) // @@ -275,10 +275,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.createTouchBar(); // Request handling - const that = this; this.marketplaceHeadersPromise = resolveMarketplaceHeaders(product.version, this.environmentService, this.fileService, { - get(key) { return that.storageService.get(key); }, - store(key, value) { that.storageService.store(key, value); } + get(key) { return storageService.get(key); }, + store(key, value) { storageService.store(key, value); } }); // Eventing @@ -361,13 +360,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { get lastFocusTime(): number { return this._lastFocusTime; } - get backupPath(): string | undefined { return this.currentConfig ? this.currentConfig.backupPath : undefined; } + get backupPath(): string | undefined { return this.currentConfig?.backupPath; } - get openedWorkspace(): IWorkspaceIdentifier | undefined { return this.currentConfig ? this.currentConfig.workspace : undefined; } + get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this.currentConfig?.workspace; } - get openedFolderUri(): URI | undefined { return this.currentConfig ? this.currentConfig.folderUri : undefined; } - - get remoteAuthority(): string | undefined { return this.currentConfig ? this.currentConfig.remoteAuthority : undefined; } + get remoteAuthority(): string | undefined { return this.currentConfig?.remoteAuthority; } setReady(): void { this._readyState = ReadyState.READY; @@ -413,9 +410,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { private registerListeners(): void { - // Crashes & Unrsponsive + // Crashes & Unrsponsive & Failed to load this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details)); this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); + this._win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this.logService.warn('Main: failed to load workbench window, ', errorDescription)); // Window close this._win.on('closed', () => { @@ -424,24 +422,42 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dispose(); }); - // Prevent loading of svgs - this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => { - if (details.url.indexOf('.svg') > 0) { - const uri = URI.parse(details.url); - if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) { + const svgFileSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { + const uri = URI.parse(details.url); + // Prevent loading of remote svgs + if (uri && uri.path.endsWith('.svg')) { + const safeScheme = svgFileSchemes.has(uri.scheme) || + uri.path.includes(Schemas.vscodeRemoteResource); + if (!safeScheme) { return callback({ cancel: true }); } } - return callback({}); + return callback({ cancel: false }); }); - this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => { + this._win.webContents.session.webRequest.onHeadersReceived((details, callback) => { const responseHeaders = details.responseHeaders as Record; - const contentType = (responseHeaders['content-type'] || responseHeaders['Content-Type']); - if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { - return callback({ cancel: true }); + + if (contentType && Array.isArray(contentType)) { + const uri = URI.parse(details.url); + // https://github.com/microsoft/vscode/issues/97564 + // ensure local svg files have Content-Type image/svg+xml + if (uri && uri.path.endsWith('.svg')) { + if (svgFileSchemes.has(uri.scheme)) { + responseHeaders['Content-Type'] = ['image/svg+xml']; + return callback({ cancel: false, responseHeaders }); + } + } + + // remote extension schemes have the following format + // http://127.0.0.1:/vscode-remote-resource?path= + if (!uri.path.includes(Schemas.vscodeRemoteResource) && + contentType.some(x => x.toLowerCase().includes('image/svg'))) { + return callback({ cancel: true }); + } } return callback({ cancel: false }); @@ -526,16 +542,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.sendWhenReady('vscode:leaveFullScreen', CancellationToken.None); }); - // Window Failed to load - this._win.webContents.on('did-fail-load', (event: Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => { - this.logService.warn('[electron event]: fail to load, ', errorDescription); - }); - // Handle configuration changes this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated())); // Handle Workspace events - this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); + this._register(this.workspacesManagementMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e))); // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; @@ -544,9 +555,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private onWindowError(error: WindowError.UNRESPONSIVE): void; - private onWindowError(error: WindowError.CRASHED, details: Details): void; - private onWindowError(error: WindowError, details?: Details): void { - this.logService.error(error === WindowError.CRASHED ? `[VS Code]: renderer process crashed (detail: ${details?.reason})` : '[VS Code]: detected unresponsive'); + private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void; + private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void { + 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 // very helpful in this case. Rather, we bring down the test run @@ -568,7 +579,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Unresponsive if (error === WindowError.UNRESPONSIVE) { if (this.isExtensionDevelopmentHost || this.isExtensionTestHost || (this._win && this._win.webContents && this._win.webContents.isDevToolsOpened())) { - // TODO@Ben Workaround for https://github.com/microsoft/vscode/issues/56994 + // TODO@bpasero Workaround for https://github.com/microsoft/vscode/issues/56994 // In certain cases the window can report unresponsiveness because a breakpoint was hit // and the process is stopped executing. The most typical cases are: // - devtools are opened and debugging happens @@ -581,9 +592,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], - message: nls.localize('appStalled', "The window is no longer responding"), - detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."), + 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) { @@ -591,6 +602,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } 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(); @@ -602,17 +614,17 @@ export class CodeWindow extends Disposable implements ICodeWindow { else { let message: string; if (details && details.reason !== 'crashed') { - message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); + message = localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); } else { - message = nls.localize('appCrashed', "The window has crashed", details?.reason); + message = localize('appCrashed', "The window has crashed", details?.reason); } this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', - buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], + buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], message, - detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), + 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) { @@ -637,37 +649,38 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Make sure to update our workspace config if we detect that it // was deleted - if (this.openedWorkspace && this.openedWorkspace.id === workspace.id && this.currentConfig) { + if (this.openedWorkspace?.id === workspace.id && this.currentConfig) { this.currentConfig.workspace = undefined; } } private onConfigurationUpdated(): void { + + // Menubar const newMenuBarVisibility = this.getMenuBarVisibility(); if (newMenuBarVisibility !== this.currentMenuBarVisibility) { this.currentMenuBarVisibility = newMenuBarVisibility; this.setMenuBarVisibility(newMenuBarVisibility); } - // Do not set to empty configuration at startup if setting is empty to not override configuration through CLI options: - const env = process.env; + + // Proxy let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() - || (env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || '').trim() // Not standardized. + || (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized. || undefined; + if (newHttpProxy?.endsWith('/')) { newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); } - const newNoProxy = (env.no_proxy || env.NO_PROXY || '').trim() || undefined; // Not standardized. + + const newNoProxy = (process.env['no_proxy'] || process.env['NO_PROXY'] || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; this.currentNoProxy = newNoProxy; + const proxyRules = newHttpProxy || ''; const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); - this._win.webContents.session.setProxy({ - proxyRules, - proxyBypassRules, - pacScript: '', - }); + this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } @@ -677,7 +690,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { + load(config: INativeWindowConfiguration, { isReload, disableExtensions }: { isReload?: boolean, disableExtensions?: boolean } = Object.create(null)): void { // If this window was loaded before from the command line // (as indicated by VSCODE_CLI environment), make sure to @@ -728,7 +741,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Load URL - perf.mark('main:loadWindow'); + mark('code/willOpenNewWindow'); this._win.loadURL(this.getUrl(configuration)); // Make window visible if it did not open in N seconds because this indicates an error @@ -747,11 +760,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._onLoad.fire(); } - reload(cli?: NativeParsedArgs): void { + async reload(cli?: NativeParsedArgs): Promise { // Copy our current config for reuse const configuration = Object.assign({}, this.currentConfig); + // Validate workspace + configuration.workspace = await this.validateWorkspace(configuration); + // Delete some properties we do not want during reload delete configuration.filesToOpenOrCreate; delete configuration.filesToDiff; @@ -761,17 +777,44 @@ export class CodeWindow extends Disposable implements ICodeWindow { // in extension development mode. These options are all development related. if (this.isExtensionDevelopmentHost && cli) { configuration.verbose = cli.verbose; + configuration.debugId = cli.debugId; configuration['inspect-extensions'] = cli['inspect-extensions']; configuration['inspect-brk-extensions'] = cli['inspect-brk-extensions']; - configuration.debugId = cli.debugId; configuration['extensions-dir'] = cli['extensions-dir']; } configuration.isInitialStartup = false; // since this is a reload // Load config - const disableExtensions = cli ? cli['disable-extensions'] : undefined; - this.load(configuration, true, disableExtensions); + this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] }); + } + + private async validateWorkspace(configuration: INativeWindowConfiguration): Promise { + + // Multi folder + if (isWorkspaceIdentifier(configuration.workspace)) { + const configPath = configuration.workspace.configPath; + if (configPath.scheme === Schemas.file) { + const workspaceExists = await this.fileService.exists(configPath); + if (!workspaceExists) { + return undefined; + } + } + } + + // Single folder + else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + const uri = configuration.workspace.uri; + if (uri.scheme === Schemas.file) { + const folderExists = await this.fileService.exists(uri); + if (!folderExists) { + return undefined; + } + } + } + + // Workspace is valid + return configuration.workspace; } private getUrl(windowConfiguration: INativeWindowConfiguration): string { @@ -780,6 +823,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; // Set zoomlevel const windowConfig = this.configurationService.getValue('window'); @@ -803,14 +847,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.maximized = this._win.isMaximized(); // Dump Perf Counters - windowConfiguration.perfEntries = perf.exportEntries(); + windowConfiguration.perfMarks = getMarks(); // Parts splash - windowConfiguration.partsSplashPath = path.join(this.environmentService.userDataPath, 'rapid_render.json'); + windowConfiguration.partsSplashPath = join(this.environmentService.userDataPath, 'rapid_render.json'); // OS Info windowConfiguration.os = { - release: os.release() + release: release() }; // Config (combination of process.argv and window configuration) @@ -848,10 +892,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { workbench = 'vs/code/electron-browser/workbench/workbench.html'; } - return (this.environmentService.sandbox ? - FileAccess._asCodeFileUri(workbench, require) : - FileAccess.asBrowserUri(workbench, require)) - .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }).toString(true); + return FileAccess + .asBrowserUri(workbench, require) + .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) + .toString(true); } serializeWindowState(): IWindowState { @@ -1148,7 +1192,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private getMenuBarVisibility(): MenuBarVisibility { let menuBarVisibility = getMenuBarVisibility(this.configurationService); if (['visible', 'toggle', 'hidden'].indexOf(menuBarVisibility) < 0) { - menuBarVisibility = 'default'; + menuBarVisibility = 'classic'; } return menuBarVisibility; @@ -1161,7 +1205,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (visibility === 'toggle') { if (notify) { - this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); + this.send('vscode:showInfoMessage', localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key.")); } } @@ -1183,7 +1227,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { const isFullscreen = this.isFullScreen; switch (visibility) { - case ('default'): + case ('classic'): this._win.setMenuBarVisibility(!isFullscreen); this._win.autoHideMenuBar = isFullscreen; break; @@ -1256,6 +1300,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { send(channel: string, ...args: any[]): void { if (this._win) { + if (this._win.isDestroyed() || this._win.webContents.isDestroyed()) { + this.logService.warn(`Sending IPC message to channel ${channel} for window that is destroyed`); + return; + } + this._win.webContents.send(channel, ...args); } } diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 9bb0b86a5b..0f8898e2cd 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -7,11 +7,10 @@ import 'vs/css!./media/issueReporter'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; -import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import * as collections from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,9 +22,11 @@ import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IMainProcessService, ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { Codicon } from 'vs/base/common/codicons'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; const MAX_URL_LENGTH = 2045; @@ -82,14 +83,12 @@ export class IssueReporter extends Disposable { this.initServices(configuration); - const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; - const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id === configuration.data.extensionId) : undefined; this.issueReporterModel = new IssueReporterModel({ issueType: configuration.data.issueType || IssueType.Bug, versionInfo: { vscodeVersion: `${configuration.product.nameShort} ${configuration.product.version} (${configuration.product.commit || 'Commit unknown'}, ${configuration.product.date || 'Date unknown'})`, - os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${isSnap ? ' snap' : ''}` + os: `${this.configuration.os.type} ${this.configuration.os.arch} ${this.configuration.os.release}${platform.isLinuxSnap ? ' snap' : ''}` }, extensionsDisabled: !!configuration.disableExtensions, fileOnExtension: configuration.data.extensionId ? !targetExtension?.isBuiltin : undefined, @@ -267,7 +266,7 @@ export class IssueReporter extends Disposable { private initServices(configuration: IssueReporterConfiguration): void { const serviceCollection = new ServiceCollection(); - const mainProcessService = new MainProcessService(configuration.windowId); + const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService; @@ -442,7 +441,11 @@ export class IssueReporter extends Disposable { private updatePreviewButtonState() { if (this.isPreviewEnabled()) { - this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + if (this.configuration.data.githubAccessToken) { + this.previewButton.label = localize('createOnGitHub', "Create on GitHub"); + } else { + this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + } this.previewButton.enabled = true; } else { this.previewButton.enabled = false; @@ -598,8 +601,7 @@ export class IssueReporter extends Disposable { issueState = $('span.issue-state'); const issueIcon = $('span.issue-icon'); - const codicon = new CodiconLabel(issueIcon); - codicon.text = issue.state === 'open' ? '$(issue-opened)' : '$(issue-closed)'; + issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed)); const issueStateLabel = $('span.issue-state.label'); issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); @@ -778,6 +780,35 @@ export class IssueReporter extends Disposable { return isValid; } + private async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string, repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.configuration.data.githubAccessToken}` + }) + }; + + return new Promise((resolve, reject) => { + window.fetch(url, init).then((response) => { + if (response.ok) { + response.json().then(result => { + ipcRenderer.send('vscode:openExternal', result.html_url); + ipcRenderer.send('vscode:closeIssueReporter'); + resolve(true); + }); + } else { + resolve(false); + } + }); + }); + } + private async createIssue(): Promise { if (!this.validateInputs()) { // If inputs are invalid, set focus to the first one and add listeners on them @@ -810,8 +841,16 @@ export class IssueReporter extends Disposable { this.hasBeenSubmitted = true; - const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); + const issueTitle = (this.getElementById('issue-title')).value; const issueBody = this.issueReporterModel.serialize(); + + const issueUrl = this.issueReporterModel.fileOnExtension() ? this.getExtensionGitHubUrl() : this.configuration.product.reportIssueUrl!; + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.configuration.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value); let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; if (url.length > MAX_URL_LENGTH) { @@ -841,6 +880,20 @@ export class IssueReporter extends Disposable { }); } + private parseGitHubUrl(url: string): undefined | { repositoryName: string, owner: string } { + // Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner. + // Repository name and owner cannot contain '/' + const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url); + if (match && match.length) { + return { + owner: match[1], + repositoryName: match[2] + }; + } + + return undefined; + } + private getExtensionGitHubUrl(): string { let repositoryUrl = ''; const bugsUrl = this.getExtensionBugsUrl(); diff --git a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css index 3ac421f113..ccbccd94f3 100644 --- a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css @@ -49,28 +49,12 @@ body { width: 90px; } -.process-item { - line-height: 22px; +.monaco-list-row:first-of-type { + border-bottom: 1px solid; } -table { - border-collapse: collapse; - width: 100%; - table-layout: fixed; -} - -th[scope='col'] { - vertical-align: bottom; - border-bottom: 1px solid #cccccc; - padding: .5rem; - border-top: 1px solid #cccccc; - cursor: default; -} - -td { - padding: .25rem; - vertical-align: top; - cursor: default; +.row { + display: flex; } .centered { @@ -79,6 +63,9 @@ td { .nameLabel{ text-align: left; + width: calc(100% - 185px); + overflow: hidden; + text-overflow: ellipsis; } .data { @@ -93,15 +80,3 @@ td { padding-left: 20px; white-space: nowrap; } - -tbody > tr:hover { - background-color: #2A2D2E; -} - -.hidden { - display: none; -} - -.header { - display: flex; -} diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html index 517805abdb..73d89eac31 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html @@ -6,7 +6,7 @@ -
+
diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 74bd1950b9..07d9967b97 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -14,38 +14,226 @@ import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; -import { addDisposableListener, $ } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; -import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ByteSize } from 'vs/platform/files/common/files'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; -interface FormattedProcessItem { - cpu: number; - memory: number; - pid: string; +class ProcessListDelegate implements IListVirtualDelegate { + getHeight(element: MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + return 22; + } + + getTemplateId(element: ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + if (isProcessItem(element)) { + return 'process'; + } + + if (isMachineProcessInformation(element)) { + return 'machine'; + } + + if (isRemoteDiagnosticError(element)) { + return 'error'; + } + + if (isProcessInformation(element)) { + return 'header'; + } + + return ''; + } +} + +interface IProcessItemTemplateData extends IProcessRowTemplateData { + CPU: HTMLElement; + memory: HTMLElement; + PID: HTMLElement; +} + +interface IProcessRowTemplateData { + name: HTMLElement; +} + +class ProcessTreeDataSource implements IDataSource { + hasChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError): boolean { + if (isRemoteDiagnosticError(element)) { + return false; + } + + if (isProcessItem(element)) { + return !!element.children?.length; + } else { + return true; + } + } + + getChildren(element: ProcessTree | ProcessInformation | MachineProcessInformation | ProcessItem | IRemoteDiagnosticError) { + if (isProcessItem(element)) { + return element.children ? element.children : []; + } + + if (isRemoteDiagnosticError(element)) { + return []; + } + + if (isProcessInformation(element)) { + // If there are multiple process roots, return these, otherwise go directly to the root process + if (element.processRoots.length > 1) { + return element.processRoots; + } else { + return [element.processRoots[0].rootProcess]; + } + } + + if (isMachineProcessInformation(element)) { + return [element.rootProcess]; + } + + return [element.processes]; + } +} + +class ProcessHeaderTreeRenderer implements ITreeRenderer { + templateId: string = 'header'; + renderTemplate(container: HTMLElement): IProcessItemTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + data.CPU = dom.append(row, dom.$('.cpu')); + data.memory = dom.append(row, dom.$('.memory')); + data.PID = dom.append(row, dom.$('.pid')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { + templateData.name.textContent = localize('name', "Process Name"); + templateData.CPU.textContent = localize('cpu', "CPU %"); + templateData.PID.textContent = localize('pid', "PID"); + templateData.memory.textContent = localize('memory', "Memory (MB)"); + + } + disposeTemplate(templateData: any): void { + // Nothing to do + } +} + +class MachineRenderer implements ITreeRenderer { + templateId: string = 'machine'; + renderTemplate(container: HTMLElement): IProcessRowTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { + templateData.name.textContent = node.element.name; + } + disposeTemplate(templateData: IProcessRowTemplateData): void { + // Nothing to do + } +} + +class ErrorRenderer implements ITreeRenderer { + templateId: string = 'error'; + renderTemplate(container: HTMLElement): IProcessRowTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + data.name = dom.append(row, dom.$('.nameLabel')); + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void { + templateData.name.textContent = node.element.errorMessage; + } + disposeTemplate(templateData: IProcessRowTemplateData): void { + // Nothing to do + } +} + + +class ProcessRenderer implements ITreeRenderer { + constructor(private platform: string, private totalMem: number, private mapPidToWindowTitle: Map) { } + + templateId: string = 'process'; + renderTemplate(container: HTMLElement): IProcessItemTemplateData { + const data = Object.create(null); + const row = dom.append(container, dom.$('.row')); + + data.name = dom.append(row, dom.$('.nameLabel')); + data.CPU = dom.append(row, dom.$('.cpu')); + data.memory = dom.append(row, dom.$('.memory')); + data.PID = dom.append(row, dom.$('.pid')); + + return data; + } + renderElement(node: ITreeNode, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void { + const { element } = node; + + let name = element.name; + if (name === 'window') { + const windowTitle = this.mapPidToWindowTitle.get(element.pid); + name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(element.pid)})` : name; + } + + templateData.name.textContent = name; + templateData.name.title = element.cmd; + + templateData.CPU.textContent = element.load.toFixed(0); + templateData.PID.textContent = element.pid.toFixed(0); + + const memory = this.platform === 'win32' ? element.mem : (this.totalMem * (element.mem / 100)); + templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0); + } + + disposeTemplate(templateData: IProcessItemTemplateData): void { + // Nothing to do + } +} + +interface MachineProcessInformation { name: string; - formattedName: string; - cmd: string; + rootProcess: ProcessItem | IRemoteDiagnosticError +} + +interface ProcessInformation { + processRoots: MachineProcessInformation[]; +} + +interface ProcessTree { + processes: ProcessInformation; +} + +function isMachineProcessInformation(item: any): item is MachineProcessInformation { + return !!item.name && !!item.rootProcess; +} + +function isProcessInformation(item: any): item is ProcessInformation { + return !!item.processRoots; +} + +function isProcessItem(item: any): item is ProcessItem { + return !!item.pid; } class ProcessExplorer { private lastRequestTime: number; - private collapsedStateCache: Map = new Map(); - private mapPidToWindowTitle = new Map(); private listeners = new DisposableStore(); private nativeHostService: INativeHostService; + private tree: DataTree | undefined; + constructor(windowId: number, private data: ProcessExplorerData) { - const mainProcessService = new MainProcessService(windowId); + const mainProcessService = new ElectronIPCMainProcessService(windowId); this.nativeHostService = new NativeHostService(windowId, mainProcessService) as INativeHostService; this.applyStyles(data.styles); @@ -56,8 +244,19 @@ class ProcessExplorer { windows.forEach(window => this.mapPidToWindowTitle.set(window.pid, window.title)); }); - ipcRenderer.on('vscode:listProcessesResponse', (event: unknown, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { - this.updateProcessInfo(processRoots); + ipcRenderer.on('vscode:listProcessesResponse', async (event: unknown, processRoots: MachineProcessInformation[]) => { + processRoots.forEach((info, index) => { + if (isProcessItem(info.rootProcess)) { + info.rootProcess.name = index === 0 ? `${this.data.applicationName} main` : 'remote agent'; + } + }); + + if (!this.tree) { + await this.createProcessTree(processRoots); + } else { + this.tree.setInput({ processes: { processRoots } }); + } + this.requestProcessList(0); }); @@ -66,54 +265,58 @@ class ProcessExplorer { ipcRenderer.send('vscode:listProcesses'); } - private getProcessList(rootProcess: ProcessItem, isLocal: boolean, totalMem: number): FormattedProcessItem[] { - const processes: FormattedProcessItem[] = []; - const handledProcesses = new Set(); - - if (rootProcess) { - this.getProcessItem(processes, rootProcess, 0, isLocal, totalMem, handledProcesses); + private async createProcessTree(processRoots: MachineProcessInformation[]): Promise { + const container = document.getElementById('process-list'); + if (!container) { + return; } - return processes; - } + const { totalmem } = await this.nativeHostService.getOSStatistics(); - private getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, indent: number, isLocal: boolean, totalMem: number, handledProcesses: Set): void { - const isRoot = (indent === 0); + const renderers = [ + new ProcessRenderer(this.data.platform, totalmem, this.mapPidToWindowTitle), + new ProcessHeaderTreeRenderer(), + new MachineRenderer(), + new ErrorRenderer() + ]; - handledProcesses.add(item.pid); + this.tree = new DataTree('processExplorer', + container, + new ProcessListDelegate(), + renderers, + new ProcessTreeDataSource(), + { + identityProvider: + { + getId: (element: ProcessTree | ProcessItem | MachineProcessInformation | ProcessInformation | IRemoteDiagnosticError) => { + if (isProcessItem(element)) { + return element.pid.toString(); + } - let name = item.name; - if (isRoot) { - name = isLocal ? `${this.data.applicationName} main` : 'remote agent'; - } + if (isRemoteDiagnosticError(element)) { + return element.hostName; + } - if (name === 'window') { - const windowTitle = this.mapPidToWindowTitle.get(item.pid); - name = windowTitle !== undefined ? `${name} (${this.mapPidToWindowTitle.get(item.pid)})` : name; - } + if (isProcessInformation(element)) { + return 'processes'; + } - // Format name with indent - const formattedName = isRoot ? name : `${' '.repeat(indent)} ${name}`; - const memory = this.data.platform === 'win32' ? item.mem : (totalMem * (item.mem / 100)); - processes.push({ - cpu: item.load, - memory: (memory / ByteSize.MB), - pid: item.pid.toFixed(0), - name, - formattedName, - cmd: item.cmd - }); + if (isMachineProcessInformation(element)) { + return element.name; + } - // Recurse into children if any - if (Array.isArray(item.children)) { - item.children.forEach(child => { - if (!child || handledProcesses.has(child.pid)) { - return; // prevent loops + return 'header'; + } } - - this.getProcessItem(processes, child, indent + 1, isLocal, totalMem, handledProcesses); }); - } + + this.tree.setInput({ processes: { processRoots } }); + this.tree.layout(window.innerHeight, window.innerWidth); + this.tree.onContextMenu(e => { + if (isProcessItem(e.element)) { + this.showContextMenu(e.element, true); + } + }); } private isDebuggable(cmd: string): boolean { @@ -121,7 +324,7 @@ class ProcessExplorer { return (matches && matches.length >= 2) || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0; } - private attachTo(item: FormattedProcessItem) { + private attachTo(item: ProcessItem) { const config: any = { type: 'node', request: 'attach', @@ -150,179 +353,16 @@ class ProcessExplorer { ipcRenderer.send('vscode:workbenchCommand', { id: 'debug.startFromConfig', from: 'processExplorer', args: [config] }); } - private getProcessIdWithHighestProperty(processList: any[], propertyName: string) { - let max = 0; - let maxProcessId; - processList.forEach(process => { - if (process[propertyName] > max) { - max = process[propertyName]; - maxProcessId = process.pid; - } - }); - - return maxProcessId; - } - - private updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: CodiconLabel, sectionName: string) { - if (shouldExpand) { - body.classList.remove('hidden'); - this.collapsedStateCache.set(sectionName, false); - twistie.text = '$(chevron-down)'; - } else { - body.classList.add('hidden'); - this.collapsedStateCache.set(sectionName, true); - twistie.text = '$(chevron-right)'; - } - } - - private renderProcessFetchError(sectionName: string, errorMessage: string) { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - const body = document.createElement('tbody'); - - this.renderProcessGroupHeader(sectionName, body, container); - - const errorRow = document.createElement('tr'); - const data = document.createElement('td'); - data.textContent = errorMessage; - data.className = 'error'; - data.colSpan = 4; - errorRow.appendChild(data); - - body.appendChild(errorRow); - container.appendChild(body); - } - - private renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) { - const headerRow = document.createElement('tr'); - - const headerData = document.createElement('td'); - headerData.colSpan = 4; - headerRow.appendChild(headerData); - - const headerContainer = document.createElement('div'); - headerContainer.className = 'header'; - headerData.appendChild(headerContainer); - - const twistieContainer = document.createElement('div'); - const twistieCodicon = new CodiconLabel(twistieContainer); - this.updateSectionCollapsedState(!this.collapsedStateCache.get(sectionName), body, twistieCodicon, sectionName); - headerContainer.appendChild(twistieContainer); - - const headerLabel = document.createElement('span'); - headerLabel.textContent = sectionName; - headerContainer.appendChild(headerLabel); - - this.listeners.add(addDisposableListener(headerData, 'click', (e) => { - const isHidden = body.classList.contains('hidden'); - this.updateSectionCollapsedState(isHidden, body, twistieCodicon, sectionName); - })); - - container.appendChild(headerRow); - } - - private renderTableSection(sectionName: string, processList: FormattedProcessItem[], renderManySections: boolean, sectionIsLocal: boolean): void { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - const highestCPUProcess = this.getProcessIdWithHighestProperty(processList, 'cpu'); - const highestMemoryProcess = this.getProcessIdWithHighestProperty(processList, 'memory'); - - const body = document.createElement('tbody'); - - if (renderManySections) { - this.renderProcessGroupHeader(sectionName, body, container); - } - - processList.forEach(p => { - const row = document.createElement('tr'); - row.id = p.pid.toString(); - - const cpu = document.createElement('td'); - p.pid === highestCPUProcess - ? cpu.classList.add('centered', 'highest') - : cpu.classList.add('centered'); - cpu.textContent = p.cpu.toFixed(0); - - const memory = document.createElement('td'); - p.pid === highestMemoryProcess - ? memory.classList.add('centered', 'highest') - : memory.classList.add('centered'); - memory.textContent = p.memory.toFixed(0); - - const pid = document.createElement('td'); - pid.classList.add('centered'); - pid.textContent = p.pid; - - const name = document.createElement('th'); - name.scope = 'row'; - name.classList.add('data'); - name.title = p.cmd; - name.textContent = p.formattedName; - - row.append(cpu, memory, pid, name); - - this.listeners.add(addDisposableListener(row, 'contextmenu', (e) => { - this.showContextMenu(e, p, sectionIsLocal); - })); - - body.appendChild(row); - }); - - container.appendChild(body); - } - - private async updateProcessInfo(processLists: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]): Promise { - const container = document.getElementById('process-list'); - if (!container) { - return; - } - - container.innerText = ''; - this.listeners.clear(); - - const tableHead = $('thead', undefined); - const row = $('tr'); - tableHead.append(row); - - row.append($('th.cpu', { scope: 'col' }, localize('cpu', "CPU %"))); - row.append($('th.memory', { scope: 'col' }, localize('memory', "Memory (MB)"))); - row.append($('th.pid', { scope: 'col' }, localize('pid', "PID"))); - row.append($('th.nameLabel', { scope: 'col' }, localize('name', "Name"))); - - container.append(tableHead); - - const hasMultipleMachines = Object.keys(processLists).length > 1; - const { totalmem } = await this.nativeHostService.getOSStatistics(); - processLists.forEach((remote, i) => { - const isLocal = i === 0; - if (isRemoteDiagnosticError(remote.rootProcess)) { - this.renderProcessFetchError(remote.name, remote.rootProcess.errorMessage); - } else { - this.renderTableSection(remote.name, this.getProcessList(remote.rootProcess, isLocal, totalmem), hasMultipleMachines, isLocal); - } - }); - } - private applyStyles(styles: ProcessExplorerStyles): void { const styleTag = document.createElement('style'); const content: string[] = []; if (styles.hoverBackground) { - content.push(`tbody > tr:hover, table > tr:hover { background-color: ${styles.hoverBackground}; }`); + content.push(`.monaco-list-row:hover { background-color: ${styles.hoverBackground}; }`); } if (styles.hoverForeground) { - content.push(`tbody > tr:hover, table > tr:hover { color: ${styles.hoverForeground}; }`); - } - - if (styles.highlightForeground) { - content.push(`.highest { color: ${styles.highlightForeground}; }`); + content.push(`.monaco-list-row:hover { color: ${styles.hoverForeground}; }`); } styleTag.textContent = content.join('\n'); @@ -334,9 +374,7 @@ class ProcessExplorer { } } - private showContextMenu(e: MouseEvent, item: FormattedProcessItem, isLocal: boolean) { - e.preventDefault(); - + private showContextMenu(item: ProcessItem, isLocal: boolean) { const items: IContextMenuItem[] = []; const pid = Number(item.pid); @@ -417,8 +455,6 @@ class ProcessExplorer { } } - - export function startup(windowId: number, data: ProcessExplorerData): void { const platformClass = data.platform === 'win32' ? 'windows' : data.platform === 'linux' ? 'linux' : 'mac'; document.body.classList.add(platformClass); // used by our fonts diff --git a/src/vs/code/electron-sandbox/proxy/auth.html b/src/vs/code/electron-sandbox/proxy/auth.html deleted file mode 100644 index 788b68fce7..0000000000 --- a/src/vs/code/electron-sandbox/proxy/auth.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - -

-
-

-
-

-

-

- - -

-
-
- - - - - diff --git a/src/vs/code/electron-sandbox/proxy/auth.js b/src/vs/code/electron-sandbox/proxy/auth.js deleted file mode 100644 index a93978cbee..0000000000 --- a/src/vs/code/electron-sandbox/proxy/auth.js +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const { ipcRenderer } = window.vscode; - -function promptForCredentials(data) { - return new Promise((c, e) => { - const $title = document.getElementById('title'); - const $username = document.getElementById('username'); - const $password = document.getElementById('password'); - const $form = document.getElementById('form'); - const $cancel = document.getElementById('cancel'); - const $message = document.getElementById('message'); - - function submit() { - c({ username: $username.value, password: $password.value }); - return false; - } - - function cancel() { - c({ username: '', password: '' }); - return false; - } - - $form.addEventListener('submit', submit); - $cancel.addEventListener('click', cancel); - - document.body.addEventListener('keydown', function (e) { - switch (e.keyCode) { - case 27: e.preventDefault(); e.stopPropagation(); return cancel(); - case 13: e.preventDefault(); e.stopPropagation(); return submit(); - } - }); - - $title.textContent = data.title; - $message.textContent = data.message; - $username.focus(); - }); -} - -ipcRenderer.on('vscode:openProxyAuthDialog', async (event, data) => { - const response = await promptForCredentials(data); - ipcRenderer.send('vscode:proxyAuthResponse', response); -}); diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 40737461d2..f36737f2bc 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -3,7 +3,8 @@ - + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 058f0c6822..364b6c5635 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -12,8 +12,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Add a perf entry right from the top - const perf = bootstrapWindow.perfLib(); - perf.mark('renderer/started'); + performance.mark('code/didStartRenderer'); // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -24,10 +23,10 @@ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - async function (workbench, configuration) { + function (_, configuration) { // Mark start of workbench - perf.mark('didLoadWorkbenchMain'); + performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); @@ -41,19 +40,34 @@ loaderConfig.recordStats = true; }, beforeRequire: function () { - perf.mark('willLoadWorkbenchMain'); + performance.mark('code/willLoadWorkbenchMain'); } } ); + // add default trustedTypes-policy for logging and to workaround + // lib/platform limitations + window.trustedTypes?.createPolicy('default', { + createHTML(value) { + // see https://github.com/electron/electron/issues/27211 + // Electron webviews use a static innerHTML default value and + // that isn't trusted. We use a default policy to check for the + // exact value of that innerHTML-string and only allow that. + if (value === '') { + return value; + } + throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached'); + // return value; + } + }); //region Helpers /** * @returns {{ - * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, - * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals'), - * perfLib: () => { mark: (name: string) => void } + * load: (modules: string[], resultCallback: (result, configuration: import('../../../platform/windows/common/windows').INativeWindowConfiguration) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') * }} */ function bootstrapWindowLib() { diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 9ef6b2afeb..5d76db4901 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -3,15 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as fs from 'fs'; +import { homedir } from 'os'; +import { constants, existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs'; import { spawn, ChildProcess, SpawnOptions } from 'child_process'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { parseCLIProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; import product from 'vs/platform/product/common/product'; -import * as paths from 'vs/base/common/path'; +import { isAbsolute, join } from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; import { isWindows, isLinux } from 'vs/base/common/platform'; @@ -69,10 +69,10 @@ export async function main(argv: string[]): Promise { // Validate if ( - !source || !target || source === target || // make sure source and target are provided and are not the same - !paths.isAbsolute(source) || !paths.isAbsolute(target) || // make sure both source and target are absolute paths - !fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file - !fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file + !source || !target || source === target || // make sure source and target are provided and are not the same + !isAbsolute(source) || !isAbsolute(target) || // make sure both source and target are absolute paths + !existsSync(source) || !statSync(source).isFile() || // make sure source exists as file + !existsSync(target) || !statSync(target).isFile() // make sure target exists as file ) { throw new Error('Using --file-write with invalid arguments.'); } @@ -83,15 +83,15 @@ export async function main(argv: string[]): Promise { let targetMode: number = 0; let restoreMode = false; if (!!args['file-chmod']) { - targetMode = fs.statSync(target).mode; - if (!(targetMode & 128) /* readonly */) { - fs.chmodSync(target, targetMode | 128); + targetMode = statSync(target).mode; + if (!(targetMode & constants.S_IWUSR)) { + chmodSync(target, targetMode | constants.S_IWUSR); restoreMode = true; } } // Write source to target - const data = fs.readFileSync(source); + const data = readFileSync(source); if (isWindows) { // On Windows we use a different strategy of saving the file // by first truncating the file and then writing with r+ mode. @@ -99,7 +99,7 @@ export async function main(argv: string[]): Promise { // (see https://github.com/microsoft/vscode/issues/931) and // prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - fs.truncateSync(target, 0); + truncateSync(target, 0); writeFileSync(target, data, { flag: 'r+' }); } else { writeFileSync(target, data); @@ -107,7 +107,7 @@ export async function main(argv: string[]): Promise { // Restore previous mode as needed if (restoreMode) { - fs.chmodSync(target, targetMode); + chmodSync(target, targetMode); } } catch (error) { error.message = `Error using --file-write: ${error.message}`; @@ -215,7 +215,7 @@ export async function main(argv: string[]): Promise { throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } - const filenamePrefix = paths.join(os.homedir(), 'prof-' + Math.random().toString(16).slice(-4)); + const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4)); addArg(argv, `--inspect-brk=${portMain}`); addArg(argv, `--remote-debugging-port=${portRenderer}`); @@ -338,7 +338,7 @@ export async function main(argv: string[]): Promise { // Make sure to delete the tmp stdin file if we have any if (stdinFilePath) { - fs.unlinkSync(stdinFilePath); + unlinkSync(stdinFilePath); } }); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 46a2411d69..5c00b7e09b 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,11 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import { release } from 'os'; +import * as fs from 'fs'; +import { gracefulify } from 'graceful-fs'; +import { isAbsolute, join } from 'vs/base/common/path'; import { raceTimeout } from 'vs/base/common/async'; -import * as semver from 'vs/base/common/semver/semver'; import product from 'vs/platform/product/common/product'; -import * as path from 'vs/base/common/path'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,416 +16,148 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { IExtensionManagementService, IExtensionGalleryService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; -import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { mkdirp, writeFile } from 'vs/base/node/pfs'; -import { getBaseLabel } from 'vs/base/common/labels'; import { IStateService } from 'vs/platform/state/node/state'; import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { URI } from 'vs/base/common/uri'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { IExtensionManifest, ExtensionType, isLanguagePackExtension, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { Schemas } from 'vs/base/common/network'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { URI } from 'vs/base/common/uri'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { VSBuffer } from 'vs/base/common/buffer'; -const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); -const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); -const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); - -function getId(manifest: IExtensionManifest, withVersion?: boolean): string { - if (withVersion) { - return `${manifest.publisher}.${manifest.name}@${manifest.version}`; - } else { - return `${manifest.publisher}.${manifest.name}`; - } -} - -const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; - -export function getIdAndVersion(id: string): [string, string | undefined] { - const matches = EXTENSION_ID_REGEX.exec(id); - if (matches && matches[1]) { - return [adoptToGalleryExtensionId(matches[1]), matches[2]]; - } - return [adoptToGalleryExtensionId(id), undefined]; -} - -type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; - -export class Main { +class CliMain extends Disposable { constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - ) { } + private argv: NativeParsedArgs + ) { + super(); - async run(argv: NativeParsedArgs): Promise { - if (argv['install-source']) { - await this.setInstallSource(argv['install-source']); - } else if (argv['list-extensions']) { - await this.listExtensions(!!argv['show-versions'], argv['category']); - } else if (argv['install-extension'] || argv['install-builtin-extension']) { - await this.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']); - } else if (argv['uninstall-extension']) { - await this.uninstallExtension(argv['uninstall-extension'], !!argv['force']); - } else if (argv['locate-extension']) { - await this.locateExtension(argv['locate-extension']); - } else if (argv['telemetry']) { - console.log(buildTelemetryMessage(this.environmentService.appRoot, this.environmentService.extensionsPath)); - } + // Enable gracefulFs + gracefulify(fs); + + this.registerListeners(); } - private setInstallSource(installSource: string): Promise { - return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30)); + private registerListeners(): void { + + // Dispose on exit + process.once('exit', () => this.dispose()); } - private async listExtensions(showVersions: boolean, category?: string): Promise { - let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); - if (category && category !== '') { - if (categories.indexOf(category.toLowerCase()) < 0) { - console.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); - return; - } - extensions = extensions.filter(e => { - if (e.manifest.categories) { - const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); - return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; - } - return false; - }); - } else if (category === '') { - console.log('Possible Categories: '); - categories.forEach(category => { - console.log(category); - }); - return; - } - extensions.forEach(e => console.log(getId(e.manifest, showVersions))); - } + async run(): Promise { - async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean): Promise { - const failed: string[] = []; - const installedExtensionsManifests: IExtensionManifest[] = []; - if (extensions.length) { - console.log(localize('installingExtensions', "Installing extensions...")); - } + // Services + const [instantiationService, appenders] = await this.initServices(); - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); - const checkIfNotInstalled = (id: string, version?: string): boolean => { - const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); - if (installedExtension) { - if (!version && !force) { - console.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); - return false; - } - if (version && installedExtension.manifest.version === version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); - return false; - } - } - return true; - }; - const vsixs: string[] = []; - const installExtensionInfos: InstallExtensionInfo[] = []; - for (const extension of extensions) { - if (/\.vsix$/i.test(extension)) { - vsixs.push(extension); - } else { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); - } - } - } - for (const extension of builtinExtensionIds) { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); - } - } + return instantiationService.invokeFunction(async accessor => { + const logService = accessor.get(ILogService); + const fileService = accessor.get(IFileService); + const environmentService = accessor.get(INativeEnvironmentService); + const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService); - if (vsixs.length) { - await Promise.all(vsixs.map(async vsix => { - try { - const manifest = await this.installVSIX(vsix, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(vsix); - } - })); - } + // Log info + logService.info('CLI main', this.argv); - if (installExtensionInfos.length) { + // Error handler + this.registerErrorHandler(logService); - const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + // Run based on argv + await this.doRun(environmentService, extensionManagementCLIService, fileService); - await Promise.all(installExtensionInfos.map(async extensionInfo => { - const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); - if (gallery) { - try { - const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(extensionInfo.id); - } - } else { - console.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); - failed.push(extensionInfo.id); - } - })); - - } - - if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { - await this.updateLocalizationsCache(); - } - - if (failed.length) { - throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); - } - } - - private async installVSIX(vsix: string, force: boolean): Promise { - vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix); - const manifest = await getManifest(vsix); - const valid = await this.validate(manifest, force); - if (valid) { - try { - await this.extensionManagementService.install(URI.file(vsix)); - console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); - return null; - } else { - throw error; - } - } - } - return null; - } - - private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { - const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); - const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); - - const galleryExtensions = new Map(); - await Promise.all([ - (async () => { - const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); - result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); - })(), - Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { - const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); - if (extension) { - galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); - } - })) - ]); - - return galleryExtensions; - } - - private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean): Promise { - const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); - const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); - if (installedExtension) { - if (galleryExtension.version === installedExtension.manifest.version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); - return null; - } - console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); - } - - try { - if (installOptions.isBuiltin) { - console.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); - } else { - console.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); - } - await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); - console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); - return null; - } else { - throw error; - } - } - } - - private async validate(manifest: IExtensionManifest, force: boolean): Promise { - if (!manifest) { - throw new Error('Invalid vsix'); - } - - const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); - - if (newer && !force) { - console.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); - return false; - } - - return true; - } - - private async uninstallExtension(extensions: string[], force: boolean): Promise { - async function getExtensionId(extensionDescription: string): Promise { - if (!/\.vsix$/i.test(extensionDescription)) { - return extensionDescription; - } - - const zipPath = path.isAbsolute(extensionDescription) ? extensionDescription : path.join(process.cwd(), extensionDescription); - const manifest = await getManifest(zipPath); - return getId(manifest); - } - - const uninstalledExtensions: ILocalExtension[] = []; - for (const extension of extensions) { - const id = await getExtensionId(extension); - const installed = await this.extensionManagementService.getInstalled(); - const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, { id })); - if (!extensionToUninstall) { - throw new Error(`${notInstalled(id)}\n${useId}`); - } - if (extensionToUninstall.type === ExtensionType.System) { - console.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id)); - return; - } - if (extensionToUninstall.isBuiltin && !force) { - console.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); - return; - } - console.log(localize('uninstalling', "Uninstalling {0}...", id)); - await this.extensionManagementService.uninstall(extensionToUninstall); - uninstalledExtensions.push(extensionToUninstall); - console.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); - } - - if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { - await this.updateLocalizationsCache(); - } - } - - private async locateExtension(extensions: string[]): Promise { - const installed = await this.extensionManagementService.getInstalled(); - extensions.forEach(e => { - installed.forEach(i => { - if (i.identifier.id === e) { - if (i.location.scheme === Schemas.file) { - console.log(i.location.fsPath); - return; - } - } - }); + // Flush the remaining data in AI adapter (with 1s timeout) + return raceTimeout(combinedAppender(...appenders).flush(), 1000); }); } - private async updateLocalizationsCache(): Promise { - const localizationService = this.instantiationService.createInstance(LocalizationsService); - await localizationService.update(); - localizationService.dispose(); - } -} + private async initServices(): Promise<[IInstantiationService, AppInsightsAppender[]]> { + const services = new ServiceCollection(); -const eventPrefix = 'adsworkbench'; // {{SQL CARBON EDIT}} + // Environment + const environmentService = new NativeEnvironmentService(this.argv); + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); -export async function main(argv: NativeParsedArgs): Promise { - const services = new ServiceCollection(); - const disposables = new DisposableStore(); + // Init folders + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); - const environmentService = new NativeEnvironmentService(argv); - const logLevel = getLogLevel(environmentService); - const loggers: ILogService[] = []; - loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel)); - if (logLevel === LogLevel.Trace) { - loggers.push(new ConsoleLogService(logLevel)); - } - const logService = new MultiplexLogService(loggers); - process.once('exit', () => logService.dispose()); - logService.info('main', argv); + // Log + const logLevel = getLogLevel(environmentService); + const loggers: ILogService[] = []; + loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel)); + if (logLevel === LogLevel.Trace) { + loggers.push(new ConsoleLogService(logLevel)); + } - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath] - .map((path): undefined | Promise => path ? mkdirp(path) : undefined)); + const logService = this._register(new MultiplexLogService(loggers)); + services.set(ILogService, logService); - // Files - const fileService = new FileService(logService); - disposables.add(fileService); - services.set(IFileService, fileService); + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); - disposables.add(configurationService); - await configurationService.initialize(); + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + services.set(IConfigurationService, configurationService); - services.set(IEnvironmentService, environmentService); - services.set(INativeEnvironmentService, environmentService); + // Init config + await configurationService.initialize(); - services.set(ILogService, logService); - services.set(IConfigurationService, configurationService); - services.set(IStateService, new SyncDescriptor(StateService)); - services.set(IProductService, { _serviceBrand: undefined, ...product }); + // State + const stateService = new StateService(environmentService, logService); + services.set(IStateService, stateService); - const instantiationService: IInstantiationService = new InstantiationService(services); - - return instantiationService.invokeFunction(async accessor => { - const stateService = accessor.get(IStateService); + // Product + services.set(IProductService, { _serviceBrand: undefined, ...product }); const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService; - const services = new ServiceCollection(); + // Request services.set(IRequestService, new SyncDescriptor(RequestService)); + + // Extensions services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); + // Localizations + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + // Telemetry const appenders: AppInsightsAppender[] = []; if (isBuilt && !extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { - appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey)); + appenders.push(new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey)); } const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), process.arch, product.commit, product.version, stateService.getItem('telemetry.machineId'), product.msftInternalDomains, installSourcePath), piiPaths: [appRoot, extensionsPath] }; @@ -434,17 +167,70 @@ export async function main(argv: NativeParsedArgs): Promise { services.set(ITelemetryService, NullTelemetryService); } - const instantiationService2 = instantiationService.createChild(services); - const main = instantiationService2.createInstance(Main); + return [new InstantiationService(services), appenders]; + } - try { - await main.run(argv); + private registerErrorHandler(logService: ILogService): void { - // Flush the remaining data in AI adapter. - // If it does not complete in 1 second, exit the process. - await raceTimeout(combinedAppender(...appenders).flush(), 1000); - } finally { - disposables.dispose(); + // Install handler for unexpected errors + setUnexpectedErrorHandler(error => { + const message = toErrorMessage(error, true); + if (!message) { + return; + } + + logService.error(`[uncaught exception in CLI]: ${message}`); + }); + } + + private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService, fileService: IFileService): Promise { + + // Install Source + if (this.argv['install-source']) { + return this.setInstallSource(environmentService, fileService, this.argv['install-source']); } - }); + + // List Extensions + if (this.argv['list-extensions']) { + return extensionManagementCLIService.listExtensions(!!this.argv['show-versions'], this.argv['category']); + } + + // Install Extension + else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) { + return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], !!this.argv['do-not-sync'], !!this.argv['force']); + } + + // Uninstall Extension + else if (this.argv['uninstall-extension']) { + return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force']); + } + + // Locate Extension + else if (this.argv['locate-extension']) { + return extensionManagementCLIService.locateExtension(this.argv['locate-extension']); + } + + // Telemetry + else if (this.argv['telemetry']) { + console.log(buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath)); + } + } + + private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] { + return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input); + } + + private async setInstallSource(environmentService: INativeEnvironmentService, fileService: IFileService, installSource: string): Promise { + await fileService.writeFile(URI.file(environmentService.installSourcePath), VSBuffer.fromString(installSource.slice(0, 30))); + } +} + +export async function main(argv: NativeParsedArgs): Promise { + const cliMain = new CliMain(argv); + + try { + await cliMain.run(); + } finally { + cliMain.dispose(); + } } diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index 8c05b40f65..8113622e7c 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -5,11 +5,12 @@ import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, platform } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { getSystemShell } from 'vs/base/node/shell'; /** * We need to get the environment from a user's shell. @@ -58,7 +59,7 @@ export async function resolveShellEnv(logService: ILogService, args: NativeParse let unixShellEnvPromise: Promise | undefined = undefined; async function doResolveUnixShellEnv(logService: ILogService): Promise { - const promise = new Promise((resolve, reject) => { + const promise = new Promise(async (resolve, reject) => { const runAsNode = process.env['ELECTRON_RUN_AS_NODE']; logService.trace('getUnixShellEnvironment#runAsNode', runAsNode); @@ -78,7 +79,8 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise 2000); return new FontInfo({ zoomLevel: browser.getZoomLevel(), + pixelRatio: browser.getPixelRatio(), fontFamily: bareFontInfo.fontFamily, fontWeight: bareFontInfo.fontWeight, fontSize: bareFontInfo.fontSize, @@ -331,15 +333,15 @@ export class Configuration extends CommonEditorConfiguration { constructor( isSimpleWidget: boolean, - options: IEditorConstructionOptions, + options: Readonly, referenceDomElement: HTMLElement | null = null, private readonly accessibilityService: IAccessibilityService ) { super(isSimpleWidget, options); - this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._onReferenceDomElementSizeChanged())); + this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._recomputeOptions())); - this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._onCSSBasedConfigurationChanged())); + this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._recomputeOptions())); if (this._validatedOptions.get(EditorOption.automaticLayout)) { this._elementSizeObserver.startObserving(); @@ -351,28 +353,24 @@ export class Configuration extends CommonEditorConfiguration { this._recomputeOptions(); } - private _onReferenceDomElementSizeChanged(): void { - this._recomputeOptions(); - } - - private _onCSSBasedConfigurationChanged(): void { - this._recomputeOptions(); - } - public observeReferenceElement(dimension?: IDimension): void { this._elementSizeObserver.observe(dimension); } - public dispose(): void { - super.dispose(); + public updatePixelRatio(): void { + this._recomputeOptions(); } - private _getExtraEditorClassName(): string { + private static _getExtraEditorClassName(): string { let extra = ''; if (!browser.isSafari && !browser.isWebkitWebView) { // Use user-select: none in all browsers except Safari and native macOS WebView extra += 'no-user-select '; } + if (browser.isSafari) { + // See https://github.com/microsoft/vscode/issues/108822 + extra += 'no-minimap-shadow '; + } if (platform.isMacintosh) { extra += 'mac '; } @@ -381,7 +379,7 @@ export class Configuration extends CommonEditorConfiguration { protected _getEnvConfiguration(): IEnvConfiguration { return { - extraEditorClassName: this._getExtraEditorClassName(), + extraEditorClassName: Configuration._getExtraEditorClassName(), outerWidth: this._elementSizeObserver.getWidth(), outerHeight: this._elementSizeObserver.getHeight(), emptySelectionClipboard: browser.isWebKit || browser.isFirefox, diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 32df91188f..d9939e8e22 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -554,6 +554,8 @@ export namespace CoreNavigationCommands { case CursorMove_.Direction.Right: case CursorMove_.Direction.Up: case CursorMove_.Direction.Down: + case CursorMove_.Direction.PrevBlankLine: + case CursorMove_.Direction.NextBlankLine: case CursorMove_.Direction.WrappedLineStart: case CursorMove_.Direction.WrappedLineFirstNonWhitespaceCharacter: case CursorMove_.Direction.WrappedLineColumnCenter: diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index bc3522c5c1..6175717e90 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -110,7 +110,12 @@ export class MouseHandler extends ViewEventHandler { return; } const e = new StandardWheelEvent(browserEvent); - if (e.browserEvent!.ctrlKey || e.browserEvent!.metaKey) { + const doMouseWheelZoom = ( + platform.isMacintosh + ? (browserEvent.metaKey && !browserEvent.ctrlKey && !browserEvent.shiftKey && !browserEvent.altKey) + : (browserEvent.ctrlKey && !browserEvent.metaKey && !browserEvent.shiftKey && !browserEvent.altKey) + ); + if (doMouseWheelZoom) { const zoomLevel: number = EditorZoom.getZoomLevel(); const delta = e.deltaY > 0 ? 1 : -1; EditorZoom.setZoomLevel(zoomLevel + delta); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 71c07167a1..1c563e0109 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -269,11 +269,11 @@ export class HitTestContext { const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); if (viewZoneWhitespace) { - let viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2, - lineCount = context.model.getLineCount(), - positionBefore: Position | null = null, - position: Position | null, - positionAfter: Position | null = null; + const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2; + const lineCount = context.model.getLineCount(); + let positionBefore: Position | null = null; + let position: Position | null; + let positionAfter: Position | null = null; if (viewZoneWhitespace.afterLineNumber !== lineCount) { // There are more lines after this view zone @@ -767,7 +767,7 @@ export class MouseTargetFactory { const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset > lineWidth) { - if (browser.isEdge && pos.column === 1) { + if (browser.isEdgeLegacy && pos.column === 1) { // See https://github.com/microsoft/vscode/issues/10875 const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)), undefined, detail); @@ -940,12 +940,16 @@ export class MouseTargetFactory { } } - // For inline decorations, Gecko returns the `` of the line and the offset is the `` with the inline decoration + // For inline decorations, Gecko sometimes returns the `` of the line and the offset is the `` with the inline decoration + // Some other times, it returns the `` with the inline decoration if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) { - const parent1 = hitResult.offsetNode.parentNode; // expected to be the view line div + const parent1 = hitResult.offsetNode.parentNode; const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (parent1).className : null; + const parent2 = parent1 ? parent1.parentNode : null; + const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; if (parent1ClassName === ViewLine.CLASS_NAME) { + // it returned the `` of the line and the offset is the `` with the inline decoration const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)]; if (tokenSpan) { const p = ctx.getPositionFromDOMInfo(tokenSpan, 0); @@ -954,6 +958,13 @@ export class MouseTargetFactory { hitTarget: null }; } + } else if (parent2ClassName === ViewLine.CLASS_NAME) { + // it returned the `` with the inline decoration + const p = ctx.getPositionFromDOMInfo(hitResult.offsetNode, 0); + return { + position: p, + hitTarget: null + }; } } @@ -1014,12 +1025,11 @@ export class MouseTargetFactory { } private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position { - const minColumn = viewModel.getLineMinColumn(position.lineNumber); const lineContent = viewModel.getLineContent(position.lineNumber); const { tabSize } = viewModel.getTextModelOptions(); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - minColumn, tabSize, Direction.Nearest); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Nearest); if (newPosition !== -1) { - return new Position(position.lineNumber, newPosition + minColumn); + return new Position(position.lineNumber, newPosition + 1); } return position; } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 0b2be31f00..0eb964a79d 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -54,7 +54,7 @@ class VisibleTextAreaData { } } -const canUseZeroSizeTextarea = (browser.isEdge || browser.isFirefox); +const canUseZeroSizeTextarea = (browser.isFirefox); export class TextAreaHandler extends ViewPart { @@ -276,17 +276,12 @@ export class TextAreaHandler extends ViewPart { this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`); this._viewController.compositionStart(); + this._context.model.onCompositionStart(); })); this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => { - if (browser.isEdge) { - // Due to isEdgeOrIE (where the textarea was not cleared initially) - // we cannot assume the text consists only of the composited text - this._visibleTextArea = this._visibleTextArea!.setWidth(0); - } else { - // adjust width by its size - this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo)); - } + // adjust width by its size + this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo)); this._render(); })); @@ -297,6 +292,7 @@ export class TextAreaHandler extends ViewPart { this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); this._viewController.compositionEnd(); + this._context.model.onCompositionEnd(); })); this._register(this._textAreaInput.onFocus(() => { diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 801bacdd35..60fcf5be6b 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -13,7 +13,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import { ITextAreaWrapper, ITypeData, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; +import { ITextAreaWrapper, ITypeData, TextAreaState, _debugComposition } from 'vs/editor/browser/controller/textAreaState'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; @@ -147,6 +147,7 @@ export class TextAreaInput extends Disposable { private readonly _host: ITextAreaInputHost; private readonly _textArea: TextAreaWrapper; private readonly _asyncTriggerCut: RunOnceScheduler; + private readonly _asyncFocusGainWriteScreenReaderContent: RunOnceScheduler; private _textAreaState: TextAreaState; private _selectionChangeListener: IDisposable | null; @@ -160,6 +161,7 @@ export class TextAreaInput extends Disposable { this._host = host; this._textArea = this._register(new TextAreaWrapper(textArea)); this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0)); + this._asyncFocusGainWriteScreenReaderContent = this._register(new RunOnceScheduler(() => this.writeScreenReaderContent('asyncFocusGain'), 0)); this._textAreaState = TextAreaState.EMPTY; this._selectionChangeListener = null; @@ -193,6 +195,10 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionstart', (e: CompositionEvent) => { + if (_debugComposition) { + console.log(`[compositionstart]`, e); + } + if (this._isDoingComposition) { return; } @@ -209,6 +215,9 @@ export class TextAreaInput extends Disposable { ) { // Handling long press case on macOS + arrow key => pretend the character was selected if (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft') { + if (_debugComposition) { + console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e); + } moveOneCharacterLeft = true; } } @@ -221,8 +230,7 @@ export class TextAreaInput extends Disposable { this._textAreaState.selectionStartPosition ? new Position(this._textAreaState.selectionStartPosition.lineNumber, this._textAreaState.selectionStartPosition.column - 1) : null, this._textAreaState.selectionEndPosition ); - } else if (!browser.isEdge) { - // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. + } else { this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY); } @@ -251,27 +259,10 @@ export class TextAreaInput extends Disposable { return [newState, typeInput]; }; - const compositionDataInValid = (locale: string): boolean => { - // https://github.com/microsoft/monaco-editor/issues/339 - // Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue. - // The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME, - // which breaks this path of code. - if (browser.isEdge && locale === 'ja') { - return true; - } - - return false; - }; - this._register(dom.addDisposableListener(textArea.domNode, 'compositionupdate', (e: CompositionEvent) => { - if (compositionDataInValid(e.locale)) { - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); - this._textAreaState = newState; - this._onType.fire(typeInput); - this._onCompositionUpdate.fire(e); - return; + if (_debugComposition) { + console.log(`[compositionupdate]`, e); } - const [newState, typeInput] = deduceComposition(e.data || ''); this._textAreaState = newState; this._onType.fire(typeInput); @@ -279,28 +270,23 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => { + if (_debugComposition) { + console.log(`[compositionend]`, e); + } // https://github.com/microsoft/monaco-editor/issues/1663 // On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data if (!this._isDoingComposition) { return; } - if (compositionDataInValid(e.locale)) { - // https://github.com/microsoft/monaco-editor/issues/339 - const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false); - this._textAreaState = newState; - this._onType.fire(typeInput); - } else { - const [newState, typeInput] = deduceComposition(e.data || ''); - this._textAreaState = newState; - this._onType.fire(typeInput); - } - // Due to - // isEdgeOrIE (where the textarea was not cleared initially) - // and isChrome (the textarea is not updated correctly when composition ends) - // and isFirefox (the textare ais not updated correctly after inserting emojis) - // we cannot assume the text at the end consists only of the composited text - if (browser.isEdge || browser.isChrome || browser.isFirefox) { + const [newState, typeInput] = deduceComposition(e.data || ''); + this._textAreaState = newState; + this._onType.fire(typeInput); + + // isChrome: the textarea is not updated correctly when composition ends + // isFirefox: the textarea is not updated correctly after inserting emojis + // => we cannot assume the text at the end consists only of the composited text + if (browser.isChrome || browser.isFirefox) { this._textAreaState = TextAreaState.readFromTextArea(this._textArea); } @@ -375,9 +361,31 @@ export class TextAreaInput extends Disposable { })); this._register(dom.addDisposableListener(textArea.domNode, 'focus', () => { + const hadFocus = this._hasFocus; + this._setHasFocus(true); + + if (browser.isSafari && !hadFocus && this._hasFocus) { + // When "tabbing into" the textarea, immediately after dispatching the 'focus' event, + // Safari will always move the selection at offset 0 in the textarea + this._asyncFocusGainWriteScreenReaderContent.schedule(); + } })); this._register(dom.addDisposableListener(textArea.domNode, 'blur', () => { + if (this._isDoingComposition) { + // See https://github.com/microsoft/vscode/issues/112621 + // where compositionend is not triggered when the editor + // is taken off-dom during a composition + + // Clear the flag to be able to write to the textarea + this._isDoingComposition = false; + + // Clear the textarea to avoid an unwanted cursor type + this.writeScreenReaderContent('blurWithoutCompositionEnd'); + + // Fire artificial composition end + this._onCompositionEnd.fire(); + } this._setHasFocus(false); })); } @@ -513,13 +521,7 @@ export class TextAreaInput extends Disposable { } if (this._hasFocus) { - if (browser.isEdge) { - // Edge has a bug where setting the selection range while the focus event - // is dispatching doesn't work. To reproduce, "tab into" the editor. - this._setAndWriteTextAreaState('focusgain', TextAreaState.EMPTY); - } else { - this.writeScreenReaderContent('focusgain'); - } + this.writeScreenReaderContent('focusgain'); } if (this._hasFocus) { diff --git a/src/vs/editor/browser/controller/textAreaState.ts b/src/vs/editor/browser/controller/textAreaState.ts index 8943e3ea59..7cec3fec0a 100644 --- a/src/vs/editor/browser/controller/textAreaState.ts +++ b/src/vs/editor/browser/controller/textAreaState.ts @@ -8,6 +8,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference } from 'vs/editor/common/model'; +export const _debugComposition = false; + export interface ITextAreaWrapper { getValue(): string; setValue(reason: string, value: string): void; @@ -59,7 +61,9 @@ export class TextAreaState { } public writeToTextArea(reason: string, textArea: ITextAreaWrapper, select: boolean): void { - // console.log(Date.now() + ': writeToTextArea ' + reason + ': ' + this.toString()); + if (_debugComposition) { + console.log('writeToTextArea ' + reason + ': ' + this.toString()); + } textArea.setValue(reason, this.value); if (select) { textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd); @@ -105,9 +109,11 @@ export class TextAreaState { }; } - // console.log('------------------------deduceInput'); - // console.log('PREVIOUS STATE: ' + previousState.toString()); - // console.log('CURRENT STATE: ' + currentState.toString()); + if (_debugComposition) { + console.log('------------------------deduceInput'); + console.log('PREVIOUS STATE: ' + previousState.toString()); + console.log('CURRENT STATE: ' + currentState.toString()); + } let previousValue = previousState.value; let previousSelectionStart = previousState.selectionStart; @@ -133,8 +139,10 @@ export class TextAreaState { currentSelectionEnd -= prefixLength; previousSelectionEnd -= prefixLength; - // console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd); - // console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd); + if (_debugComposition) { + console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd); + console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd); + } if (couldBeEmojiInput && currentSelectionStart === currentSelectionEnd && previousValue.length > 0) { // on OSX, emojis from the emoji picker are inserted at random locations @@ -196,7 +204,9 @@ export class TextAreaState { // no current selection const replacePreviousCharacters = (previousPrefix.length - prefixLength); - // console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars'); + if (_debugComposition) { + console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars'); + } return { text: currentValue, diff --git a/src/vs/editor/browser/core/markdownRenderer.ts b/src/vs/editor/browser/core/markdownRenderer.ts index cc754843ae..f629f2664e 100644 --- a/src/vs/editor/browser/core/markdownRenderer.ts +++ b/src/vs/editor/browser/core/markdownRenderer.ts @@ -88,9 +88,7 @@ export class MarkdownRenderer { const element = document.createElement('span'); - element.innerHTML = MarkdownRenderer._ttpTokenizer - ? MarkdownRenderer._ttpTokenizer.createHTML(value, tokenization) as unknown as string - : tokenizeToString(value, tokenization); + element.innerHTML = (MarkdownRenderer._ttpTokenizer?.createHTML(value, tokenization) ?? tokenizeToString(value, tokenization)) as string; // use "good" font let fontFamily = this._options.codeBlockFontFamily; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 402f61d166..a86d227ab9 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -361,6 +361,11 @@ export interface IEditorConstructionOptions extends IEditorOptions { } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: editorCommon.IDimension; + /** * Place overflow widgets inside an external DOM node. * Defaults to an internal DOM node. @@ -684,10 +689,15 @@ export interface ICodeEditor extends editorCommon.IEditor { executeCommand(source: string | null | undefined, command: editorCommon.ICommand): void; /** - * Push an "undo stop" in the undo-redo stack. + * Create an "undo stop" in the undo-redo stack. */ pushUndoStop(): boolean; + /** + * Remove the "undo stop" in the undo-redo stack. + */ + popUndoStop(): boolean; + /** * Execute edits on the editor. * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. @@ -1042,7 +1052,7 @@ export interface IDiffEditor extends editorCommon.IEditor { /** *@internal */ -export function isCodeEditor(thing: any): thing is ICodeEditor { +export function isCodeEditor(thing: unknown): thing is ICodeEditor { if (thing && typeof (thing).getEditorType === 'function') { return (thing).getEditorType() === editorCommon.EditorType.ICodeEditor; } else { @@ -1053,7 +1063,7 @@ export function isCodeEditor(thing: any): thing is ICodeEditor { /** *@internal */ -export function isDiffEditor(thing: any): thing is IDiffEditor { +export function isDiffEditor(thing: unknown): thing is IDiffEditor { if (thing && typeof (thing).getEditorType === 'function') { return (thing).getEditorType() === editorCommon.EditorType.IDiffEditor; } else { @@ -1064,8 +1074,8 @@ export function isDiffEditor(thing: any): thing is IDiffEditor { /** *@internal */ -export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeCodeEditor { - return thing +export function isCompositeEditor(thing: unknown): thing is editorCommon.ICompositeCodeEditor { + return !!thing && typeof thing === 'object' && typeof (thing).onDidChangeActiveEditor === 'function'; @@ -1074,7 +1084,7 @@ export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeC /** *@internal */ -export function getCodeEditor(thing: any): ICodeEditor | null { +export function getCodeEditor(thing: unknown): ICodeEditor | null { if (isCodeEditor(thing)) { return thing; } diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 6a80a097a7..c6aafad884 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -73,6 +73,7 @@ export interface IBulkEditOptions { quotableLabel?: string; undoRedoSource?: UndoRedoSource; undoRedoGroupId?: number; + confirmBeforeUndo?: boolean; } export interface IBulkEditResult { diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index 684152388f..e0cd695b58 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -347,6 +347,8 @@ const _CSS_MAP: { [prop: string]: string; } = { fontStyle: 'font-style:{0};', fontWeight: 'font-weight:{0};', + fontSize: 'font-size:{0};', + fontFamily: 'font-family:{0};', textDecoration: 'text-decoration:{0};', cursor: 'cursor:{0};', letterSpacing: 'letter-spacing:{0};', @@ -357,6 +359,7 @@ const _CSS_MAP: { [prop: string]: string; } = { contentText: 'content:\'{0}\';', contentIconPath: 'content:{0};', margin: 'margin:{0};', + padding: 'padding:{0};', width: 'width:{0};', height: 'height:{0};' }; @@ -529,7 +532,7 @@ class DecorationCSSRules { cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped)); } - this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin'], cssTextArr); + this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin', 'padding'], cssTextArr); if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) { cssTextArr.push('display:inline-block;'); } diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 745b162c83..ec304905c2 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; +import { ResourceMap } from 'vs/base/common/map'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import { normalizePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import { EditorOpenContext } from 'vs/platform/editor/common/editor'; -import { ResourceMap } from 'vs/base/common/map'; - +import { IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, matchesScheme, OpenOptions, ResolveExternalUriOptions } from 'vs/platform/opener/common/opener'; class CommandOpener implements IOpener { @@ -105,15 +105,16 @@ export class OpenerService implements IOpenerService { private readonly _resolvers = new LinkedList(); private readonly _resolvedUriTargets = new ResourceMap(uri => uri.with({ path: null, fragment: null, query: null }).toString()); - private _externalOpener: IExternalOpener; + private _defaultExternalOpener: IExternalOpener; + private readonly _externalOpeners = new LinkedList(); constructor( @ICodeEditorService editorService: ICodeEditorService, @ICommandService commandService: ICommandService, ) { // Default external opener is going through window.open() - this._externalOpener = { - openExternal: href => { + this._defaultExternalOpener = { + openExternal: async href => { // ensure to open HTTP/HTTPS links into new windows // to not trigger a navigation. Any other link is // safe to be set as HREF to prevent a blank window @@ -123,11 +124,11 @@ export class OpenerService implements IOpenerService { } else { window.location.href = href; } - return Promise.resolve(true); + return true; } }; - // Default opener: maito, http(s), command, and catch-all-editors + // Default opener: any external, maito, http(s), command, and catch-all-editors this._openers.push({ open: async (target: URI | string, options?: OpenOptions) => { if (options?.openExternal || matchesScheme(target, Schemas.mailto) || matchesScheme(target, Schemas.http) || matchesScheme(target, Schemas.https)) { @@ -157,8 +158,13 @@ export class OpenerService implements IOpenerService { return { dispose: remove }; } - setExternalOpener(externalOpener: IExternalOpener): void { - this._externalOpener = externalOpener; + setDefaultExternalOpener(externalOpener: IExternalOpener): void { + this._defaultExternalOpener = externalOpener; + } + + registerExternalOpener(opener: IExternalOpener): IDisposable { + const remove = this._externalOpeners.push(opener); + return { dispose: remove }; } async open(target: URI | string, options?: OpenOptions): Promise { @@ -201,13 +207,29 @@ export class OpenerService implements IOpenerService { const uri = typeof resource === 'string' ? URI.parse(resource) : resource; const { resolved } = await this.resolveExternalUri(uri, options); + let href: string; if (typeof resource === 'string' && uri.toString() === resolved.toString()) { // open the url-string AS IS - return this._externalOpener.openExternal(resource); + href = resource; } else { // open URI using the toString(noEncode)+encodeURI-trick - return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); + href = encodeURI(resolved.toString(true)); } + + if (options?.allowContributedOpeners) { + const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined; + for (const opener of this._externalOpeners) { + const didOpen = await opener.openExternal(href, { + sourceUri: uri, + preferredOpenerId, + }, CancellationToken.None); + if (didOpen) { + return true; + } + } + } + + return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None); } dispose() { diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 4d9c89f468..3ad84da5f3 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -111,8 +111,8 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe allVisibleColumns[i] = tmp[1]; } const html = sb.build(); - const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; - containerDomNode.innerHTML = trustedhtml as unknown as string; + const trustedhtml = ttPolicy?.createHTML(html) ?? html; + containerDomNode.innerHTML = trustedhtml as string; containerDomNode.style.position = 'absolute'; containerDomNode.style.top = '10000'; @@ -232,11 +232,11 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: if (strings.isFullWidthCharacter(charCode)) { charWidth++; } - // if (renderControlCharacters && charCode < 32) { - // sb.write1(9216 + charCode); - // } else { - sb.write1(charCode); - // } + if (charCode < 32) { + sb.write1(9216 + charCode); + } else { + sb.write1(charCode); + } } charOffset += producedCharacters; diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index f1095ff8c9..ff6854c6c6 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import * as browser from 'vs/base/browser/browser'; import { Selection } from 'vs/editor/common/core/selection'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -65,6 +66,7 @@ export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; + private _configPixelRatio: number; private _selections: Selection[]; // The view lines @@ -104,6 +106,7 @@ export class View extends ViewEventHandler { // The view context is passed on to most classes (basically to reduce param. counts in ctors) this._context = new ViewContext(configuration, themeService.getColorTheme(), model); + this._configPixelRatio = this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio); // Ensure the view is the first event handler in order to update the layout this._context.addEventHandler(this); @@ -298,6 +301,7 @@ export class View extends ViewEventHandler { this._scheduleRender(); } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this._configPixelRatio = this._context.configuration.options.get(EditorOption.pixelRatio); this.domNode.setClassName(this._getEditorClassName()); this._applyLayout(); return false; @@ -330,8 +334,8 @@ export class View extends ViewEventHandler { this._viewLines.dispose(); // Destroy view parts - for (let i = 0, len = this._viewParts.length; i < len; i++) { - this._viewParts[i].dispose(); + for (const viewPart of this._viewParts) { + viewPart.dispose(); } super.dispose(); @@ -354,8 +358,7 @@ export class View extends ViewEventHandler { private _getViewPartsToRender(): ViewPart[] { let result: ViewPart[] = [], resultLen = 0; - for (let i = 0, len = this._viewParts.length; i < len; i++) { - const viewPart = this._viewParts[i]; + for (const viewPart of this._viewParts) { if (viewPart.shouldRender()) { result[resultLen++] = viewPart; } @@ -401,16 +404,20 @@ export class View extends ViewEventHandler { const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this._viewLines); // Render the rest of the parts - for (let i = 0, len = viewPartsToRender.length; i < len; i++) { - const viewPart = viewPartsToRender[i]; + for (const viewPart of viewPartsToRender) { viewPart.prepareRender(renderingContext); } - for (let i = 0, len = viewPartsToRender.length; i < len; i++) { - const viewPart = viewPartsToRender[i]; + for (const viewPart of viewPartsToRender) { viewPart.render(renderingContext); viewPart.onDidRender(); } + + // Try to detect browser zooming and paint again if necessary + if (Math.abs(browser.getPixelRatio() - this._configPixelRatio) > 0.001) { + // looks like the pixel ratio has changed + this._context.configuration.updatePixelRatio(); + } } // --- BEGIN CodeEditor helpers @@ -462,8 +469,7 @@ export class View extends ViewEventHandler { if (everything) { // Force everything to render... this._viewLines.forceShouldRender(); - for (let i = 0, len = this._viewParts.length; i < len; i++) { - const viewPart = this._viewParts[i]; + for (const viewPart of this._viewParts) { viewPart.forceShouldRender(); } } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 14ba462530..4042232fe0 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -506,15 +506,15 @@ class ViewLayerRenderer { ctx.lines.splice(removeIndex, removeCount); } - private _finishRenderingNewLines(ctx: IRendererContext, domNodeIsEmpty: boolean, newLinesHTML: string, wasNew: boolean[]): void { + private _finishRenderingNewLines(ctx: IRendererContext, domNodeIsEmpty: boolean, newLinesHTML: string | TrustedHTML, wasNew: boolean[]): void { if (ViewLayerRenderer._ttPolicy) { - newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML) as unknown as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393 + newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML as string); } const lastChild = this.domNode.lastChild; if (domNodeIsEmpty || !lastChild) { - this.domNode.innerHTML = newLinesHTML; + this.domNode.innerHTML = newLinesHTML as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393; } else { - lastChild.insertAdjacentHTML('afterend', newLinesHTML); + lastChild.insertAdjacentHTML('afterend', newLinesHTML as string); } let currChild = this.domNode.lastChild; @@ -527,13 +527,13 @@ class ViewLayerRenderer { } } - private _finishRenderingInvalidLines(ctx: IRendererContext, invalidLinesHTML: string, wasInvalid: boolean[]): void { + private _finishRenderingInvalidLines(ctx: IRendererContext, invalidLinesHTML: string | TrustedHTML, wasInvalid: boolean[]): void { const hugeDomNode = document.createElement('div'); if (ViewLayerRenderer._ttPolicy) { - invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML) as unknown as string; + invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML as string); } - hugeDomNode.innerHTML = invalidLinesHTML; + hugeDomNode.innerHTML = invalidLinesHTML as string; for (let i = 0; i < ctx.linesLength; i++) { const line = ctx.lines[i]; diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 91a57be1a2..c4d3398df4 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -42,8 +42,8 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { this._contentWidth = layoutInfo.contentWidth; this._selectionIsEmpty = true; this._focused = false; - this._cursorLineNumbers = []; - this._selections = []; + this._cursorLineNumbers = [1]; + this._selections = [new Selection(1, 1, 1, 1)]; this._renderData = null; this._context.addEventHandler(this); diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index bd46aff4f2..3d185567cc 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -28,6 +28,7 @@ export class LineNumbersOverlay extends DynamicViewOverlay { private _lineNumbersWidth!: number; private _lastCursorModelPosition: Position; private _renderResult: string[] | null; + private _activeLineNumber: number; constructor(context: ViewContext) { super(); @@ -37,6 +38,7 @@ export class LineNumbersOverlay extends DynamicViewOverlay { this._lastCursorModelPosition = new Position(1, 1); this._renderResult = null; + this._activeLineNumber = 1; this._context.addEventHandler(this); } @@ -68,10 +70,15 @@ export class LineNumbersOverlay extends DynamicViewOverlay { const primaryViewPosition = e.selections[0].getPosition(); this._lastCursorModelPosition = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(primaryViewPosition); - if (this._renderLineNumbers === RenderLineNumbersType.Relative || this._renderLineNumbers === RenderLineNumbersType.Interval) { - return true; + let shouldRender = false; + if (this._activeLineNumber !== primaryViewPosition.lineNumber) { + this._activeLineNumber = primaryViewPosition.lineNumber; + shouldRender = true; } - return false; + if (this._renderLineNumbers === RenderLineNumbersType.Relative || this._renderLineNumbers === RenderLineNumbersType.Interval) { + shouldRender = true; + } + return shouldRender; } public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; @@ -135,7 +142,7 @@ export class LineNumbersOverlay extends DynamicViewOverlay { const lineHeightClassName = (platform.isLinux ? (this._lineHeight % 2 === 0 ? ' lh-even' : ' lh-odd') : ''); const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; - const common = '
'; + const common = '
'; const lineCount = this._context.model.getLineCount(); const output: string[] = []; @@ -153,11 +160,19 @@ export class LineNumbersOverlay extends DynamicViewOverlay { const renderLineNumber = this._getLineRenderLineNumber(lineNumber); if (renderLineNumber) { - output[lineIndex] = ( - common - + renderLineNumber - + '
' - ); + if (lineNumber === this._activeLineNumber) { + output[lineIndex] = ( + '
' + + renderLineNumber + + '
' + ); + } else { + output[lineIndex] = ( + common + + renderLineNumber + + '
' + ); + } } else { output[lineIndex] = ''; } @@ -187,6 +202,6 @@ registerThemingParticipant((theme, collector) => { } const activeLineNumber = theme.getColor(editorActiveLineNumber); if (activeLineNumber) { - collector.addRule(`.monaco-editor .current-line ~ .line-numbers { color: ${activeLineNumber}; }`); + collector.addRule(`.monaco-editor .line-numbers.active-line-number { color: ${activeLineNumber}; }`); } }); diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 00f71203e1..a1e8c1dd04 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -44,7 +44,7 @@ const canUseFastRenderedViewLine = (function () { let monospaceAssumptionsAreValid = true; -const alwaysRenderInlineSelection = (browser.isEdge); +const alwaysRenderInlineSelection = (browser.isEdgeLegacy); export class DomReadingContext { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index 614543e790..cbfa190288 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -25,3 +25,8 @@ left: -6px; width: 6px; } +.monaco-editor.no-minimap-shadow .minimap-shadow-visible { + position: absolute; + left: -1px; + width: 1px; +} diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 56e5c20241..a4148fa64d 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -237,6 +237,7 @@ class MinimapLayout { options: MinimapOptions, viewportStartLineNumber: number, viewportEndLineNumber: number, + viewportStartLineNumberVerticalOffset: number, viewportHeight: number, viewportContainsWhitespaceGaps: boolean, lineCount: number, @@ -331,7 +332,8 @@ class MinimapLayout { } const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1); - const sliderTopAligned = (scrollTop / lineHeight - startLineNumber + 1) * minimapLineHeight / pixelRatio; + const partialLine = (scrollTop - viewportStartLineNumberVerticalOffset) / lineHeight; + const sliderTopAligned = (viewportStartLineNumber - startLineNumber + partialLine) * minimapLineHeight / pixelRatio; return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTopAligned, sliderHeight, startLineNumber, endLineNumber); } @@ -505,6 +507,7 @@ interface IMinimapRenderingContext { readonly viewportStartLineNumber: number; readonly viewportEndLineNumber: number; + readonly viewportStartLineNumberVerticalOffset: number; readonly scrollTop: number; readonly scrollLeft: number; @@ -859,6 +862,7 @@ export class Minimap extends ViewPart implements IMinimapModel { } } public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean { + this._onOptionsMaybeChanged(); return this._actual.onTokensColorsChanged(); } public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { @@ -891,6 +895,7 @@ export class Minimap extends ViewPart implements IMinimapModel { viewportStartLineNumber: viewportStartLineNumber, viewportEndLineNumber: viewportEndLineNumber, + viewportStartLineNumberVerticalOffset: ctx.getVerticalOffsetForLineNumber(viewportStartLineNumber), scrollTop: ctx.scrollTop, scrollLeft: ctx.scrollLeft, @@ -1344,6 +1349,7 @@ class InnerMinimap extends Disposable { this._model.options, renderingCtx.viewportStartLineNumber, renderingCtx.viewportEndLineNumber, + renderingCtx.viewportStartLineNumberVerticalOffset, renderingCtx.viewportHeight, renderingCtx.viewportContainsWhitespaceGaps, this._model.getLineCount(), diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index 1049b07071..41e7b130d3 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -60,7 +60,7 @@ function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle { // TODO@Alex: Remove this once IE11 fixes Bug #524217 // The problem in IE11 is that it does some sort of auto-zooming to accomodate for displays with different pixel density. // Unfortunately, this auto-zooming is buggy around dealing with rounded borders -const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdge; +const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdgeLegacy; export class SelectionsOverlay extends DynamicViewOverlay { diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index e81b971389..4f969c7c27 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -25,6 +25,7 @@ export class ViewCursors extends ViewPart { private _cursorStyle: TextEditorCursorStyle; private _cursorSmoothCaretAnimation: boolean; private _selectionIsEmpty: boolean; + private _isComposingInput: boolean; private _isVisible: boolean; @@ -49,6 +50,7 @@ export class ViewCursors extends ViewPart { this._cursorStyle = options.get(EditorOption.cursorStyle); this._cursorSmoothCaretAnimation = options.get(EditorOption.cursorSmoothCaretAnimation); this._selectionIsEmpty = true; + this._isComposingInput = false; this._isVisible = false; @@ -83,7 +85,16 @@ export class ViewCursors extends ViewPart { } // --- begin event handlers - + public onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { + this._isComposingInput = true; + this._updateBlinking(); + return true; + } + public onCompositionEnd(e: viewEvents.ViewCompositionEndEvent): boolean { + this._isComposingInput = false; + this._updateBlinking(); + return true; + } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; @@ -195,6 +206,10 @@ export class ViewCursors extends ViewPart { // ---- blinking logic private _getCursorBlinking(): TextEditorCursorBlinkingStyle { + if (this._isComposingInput) { + // avoid double cursors + return TextEditorCursorBlinkingStyle.Hidden; + } if (!this._editorHasFocus) { return TextEditorCursorBlinkingStyle.Hidden; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 939f03bcb3..67db9d4932 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -21,10 +21,10 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; import { IContentWidgetData, IOverlayWidgetData, View } from 'vs/editor/browser/view/viewImpl'; import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; -import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations, InDiffEditorState } from 'vs/editor/common/config/editorOptions'; +import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { Cursor } from 'vs/editor/common/controller/cursor'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; -import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -241,7 +241,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE constructor( domElement: HTMLElement, - options: editorBrowser.IEditorConstructionOptions, + _options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -253,10 +253,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ) { super(); - options = options || {}; + const options = { ..._options }; this._domElement = domElement; this._overflowWidgetsDomNode = options.overflowWidgetsDomNode; + delete options.overflowWidgetsDomNode; this._id = (++EDITOR_ID); this._decorationTypeKeysToIds = {}; this._decorationTypeSubtypes = {}; @@ -331,7 +332,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._codeEditorService.addCodeEditor(this); } - protected _createConfiguration(options: editorBrowser.IEditorConstructionOptions, accessibilityService: IAccessibilityService): editorCommon.IConfiguration { + protected _createConfiguration(options: Readonly, accessibilityService: IAccessibilityService): editorCommon.IConfiguration { return new Configuration(this.isSimpleWidget, options, this._domElement, accessibilityService); } @@ -366,7 +367,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._instantiationService.invokeFunction(fn); } - public updateOptions(newOptions: IEditorOptions): void { + public updateOptions(newOptions: Readonly): void { this._configuration.updateOptions(newOptions); } @@ -811,7 +812,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ); } - public setSelections(ranges: readonly ISelection[], source: string = 'api'): void { + public setSelections(ranges: readonly ISelection[], source: string = 'api', reason = CursorChangeReason.NotSet): void { if (!this._modelData) { return; } @@ -823,7 +824,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE throw new Error('Invalid arguments'); } } - this._modelData.viewModel.setSelections(source, ranges); + this._modelData.viewModel.setSelections(source, ranges, reason); } public getContentWidth(): number { @@ -1122,6 +1123,18 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return true; } + public popUndoStop(): boolean { + if (!this._modelData) { + return false; + } + if (this._configuration.options.get(EditorOption.readOnly)) { + // read only editor => sorry! + return false; + } + this._modelData.model.popStackElement(); + return true; + } + public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean { if (!this._modelData) { return false; @@ -1774,7 +1787,7 @@ class EditorContextKeysManager extends Disposable { this._editorTabMovesFocus.set(options.get(EditorOption.tabFocusMode)); this._editorReadonly.set(options.get(EditorOption.readOnly)); - this._inDiffEditor.set(options.get(EditorOption.inDiffEditor) !== InDiffEditorState.None); + this._inDiffEditor.set(options.get(EditorOption.inDiffEditor)); this._editorColumnSelection.set(options.get(EditorOption.columnSelection)); } @@ -1822,6 +1835,7 @@ export class EditorModeContext extends Disposable { private readonly _hasMultipleDocumentFormattingProvider: IContextKey; private readonly _hasMultipleDocumentSelectionFormattingProvider: IContextKey; private readonly _hasSignatureHelpProvider: IContextKey; + private readonly _hasInlineHintsProvider: IContextKey; private readonly _isInWalkThrough: IContextKey; constructor( @@ -1844,6 +1858,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(_contextKeyService); this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(_contextKeyService); this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(_contextKeyService); + this._hasInlineHintsProvider = EditorContextKeys.hasInlineHintsProvider.bindTo(_contextKeyService); this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); @@ -1872,6 +1887,7 @@ export class EditorModeContext extends Disposable { this._register(modes.DocumentFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.DocumentRangeFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.SignatureHelpProviderRegistry.onDidChange(update)); + this._register(modes.InlineHintsProviderRegistry.onDidChange(update)); update(); } @@ -1923,6 +1939,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); + this._hasInlineHintsProvider.set(modes.InlineHintsProviderRegistry.has(model)); this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length + modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index b130b71f8d..0e382a40b2 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -12,15 +12,14 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import * as objects from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { Configuration } from 'vs/editor/browser/config/configuration'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { IDiffEditorOptions, IEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption, InDiffEditorState } from 'vs/editor/common/config/editorOptions'; +import { IDiffEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; @@ -55,6 +54,11 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +export interface IDiffCodeEditorWidgetOptions { + originalEditor?: ICodeEditorWidgetOptions; + modifiedEditor?: ICodeEditorWidgetOptions; +} + interface IEditorDiffDecorations { decorations: IModelDeltaDecoration[]; overviewZones: OverviewRulerZone[]; @@ -110,7 +114,7 @@ class VisualEditorState { this._decorations = editor.deltaDecorations(this._decorations, []); } - public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void { + public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler | null, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void { const scrollState = restoreScrollState ? StableEditorScrollState.capture(editor) : null; @@ -156,8 +160,8 @@ class VisualEditorState { let DIFF_EDITOR_ID = 0; -const diffInsertIcon = registerIcon('diff-insert', Codicon.add, nls.localize('diffInsertIcon', 'Line decoration for inserts in the diff editor')); -const diffRemoveIcon = registerIcon('diff-remove', Codicon.remove, nls.localize('diffRemoveIcon', 'Line decoration for removals in the diff editor')); +const diffInsertIcon = registerIcon('diff-insert', Codicon.add, nls.localize('diffInsertIcon', 'Line decoration for inserts in the diff editor.')); +const diffRemoveIcon = registerIcon('diff-remove', Codicon.remove, nls.localize('diffRemoveIcon', 'Line decoration for removals in the diff editor.')); const ttPolicy = window.trustedTypes?.createPolicy('diffEditorWidget', { createHTML: value => value }); export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffEditor { @@ -213,8 +217,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _maxComputationTime: number; private _renderIndicators: boolean; private _enableSplitViewResizing: boolean; - private _wordWrap: 'off' | 'on' | 'wordWrapColumn' | 'bounded' | undefined; - private _wordWrapMinified: boolean | undefined; + private _renderOverviewRuler: boolean; private _strategy!: DiffEditorWidgetStyle; private readonly _updateDecorationsRunner: RunOnceScheduler; @@ -230,7 +233,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, - options: editorBrowser.IDiffEditorConstructionOptions, + options: Readonly, + codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @@ -259,9 +263,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._domElement = domElement; options = options || {}; - this._wordWrap = options.wordWrap; - this._wordWrapMinified = options.wordWrapMinified; - // renderSideBySide this._renderSideBySide = true; if (typeof options.renderSideBySide !== 'undefined') { @@ -296,6 +297,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._contextKeyService.createKey('isInEmbeddedDiffEditor', false); } + this._renderOverviewRuler = true; + if (typeof options.renderOverviewRuler !== 'undefined') { + this._renderOverviewRuler = Boolean(options.renderOverviewRuler); + } + this._updateDecorationsRunner = this._register(new RunOnceScheduler(() => this._updateDecorations(), 0)); this._containerDomElement = document.createElement('div'); @@ -317,7 +323,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._register(dom.addStandardDisposableListener(this._overviewDomElement, 'mousedown', (e) => { this._modifiedEditor.delegateVerticalScrollbarMouseDown(e); })); - this._containerDomElement.appendChild(this._overviewDomElement); + if (this._renderOverviewRuler) { + this._containerDomElement.appendChild(this._overviewDomElement); + } // Create left side this._originalDomNode = document.createElement('div'); @@ -343,7 +351,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._isVisible = true; this._isHandlingScrollEvent = false; - this._elementSizeObserver = this._register(new ElementSizeObserver(this._containerDomElement, undefined, () => this._onDidContainerSizeChanged())); + this._elementSizeObserver = this._register(new ElementSizeObserver(this._containerDomElement, options.dimension, () => this._onDidContainerSizeChanged())); if (options.automaticLayout) { this._elementSizeObserver.startObserving(); } @@ -362,8 +370,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE rightServices.set(IContextKeyService, rightContextKeyService); const rightScopedInstantiationService = instantiationService.createChild(rightServices); - this._originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService, leftContextKeyService); - this._modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService, rightContextKeyService); + this._originalEditor = this._createLeftHandSideEditor(options, codeEditorWidgetOptions.originalEditor || {}, leftScopedInstantiationService, leftContextKeyService); + this._modifiedEditor = this._createRightHandSideEditor(options, codeEditorWidgetOptions.modifiedEditor || {}, rightScopedInstantiationService, rightContextKeyService); this._originalOverviewRuler = null; this._modifiedOverviewRuler = null; @@ -424,6 +432,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor.getContentHeight(); } + public getViewWidth(): number { + return this._elementSizeObserver.getWidth(); + } + private _setState(newState: editorBrowser.DiffEditorState): void { if (this._state === newState) { return; @@ -462,6 +474,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _recreateOverviewRulers(): void { + if (!this._renderOverviewRuler) { + return; + } + if (this._originalOverviewRuler) { this._overviewDomElement.removeChild(this._originalOverviewRuler.getDomNode()); this._originalOverviewRuler.dispose(); @@ -483,8 +499,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._layoutOverviewRulers(); } - private _createLeftHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { - const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options)); + private _createLeftHandSideEditor(options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { + const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options), codeEditorWidgetOptions); this._register(editor.onDidScrollChange((e) => { if (this._isHandlingScrollEvent) { @@ -545,8 +561,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return editor; } - private _createRightHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { - const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options)); + private _createRightHandSideEditor(options: Readonly, codeEditorWidgetOptions: ICodeEditorWidgetOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { + const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options), codeEditorWidgetOptions); this._register(editor.onDidScrollChange((e) => { if (this._isHandlingScrollEvent) { @@ -613,8 +629,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return editor; } - protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: IEditorOptions): CodeEditorWidget { - return instantiationService.createInstance(CodeEditorWidget, container, options, {}); + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget { + return instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions); } public dispose(): void { @@ -636,7 +652,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._modifiedOverviewRuler.dispose(); } this._overviewDomElement.removeChild(this._overviewViewportDomElement.domNode); - this._containerDomElement.removeChild(this._overviewDomElement); + if (this._renderOverviewRuler) { + this._containerDomElement.removeChild(this._overviewDomElement); + } this._containerDomElement.removeChild(this._originalDomNode); this._originalEditor.dispose(); @@ -687,10 +705,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._modifiedEditor; } - public updateOptions(newOptions: IDiffEditorOptions): void { - - this._wordWrap = typeof newOptions.wordWrap !== 'undefined' ? newOptions.wordWrap : this._wordWrap; - this._wordWrapMinified = typeof newOptions.wordWrapMinified !== 'undefined' ? newOptions.wordWrapMinified : this._wordWrapMinified; + public updateOptions(newOptions: Readonly): void { // Handle side by side let renderSideBySideChanged = false; @@ -752,6 +767,16 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE // Update class name this._containerDomElement.className = DiffEditorWidget._getClassName(this._themeService.getColorTheme(), this._renderSideBySide); } + + // renderOverviewRuler + if (typeof newOptions.renderOverviewRuler !== 'undefined' && this._renderOverviewRuler !== newOptions.renderOverviewRuler) { + this._renderOverviewRuler = newOptions.renderOverviewRuler; + if (this._renderOverviewRuler) { + this._containerDomElement.appendChild(this._overviewDomElement); + } else { + this._containerDomElement.removeChild(this._overviewDomElement); + } + } } public getModel(): editorCommon.IDiffEditorModel { @@ -761,7 +786,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE }; } - public setModel(model: editorCommon.IDiffEditorModel): void { + public setModel(model: editorCommon.IDiffEditorModel | null): void { // Guard us against partial null model if (model && (!model.original || !model.modified)) { throw new Error(!model.original ? 'DiffEditorWidget.setModel: Original model is null' : 'DiffEditorWidget.setModel: Modified model is null'); @@ -923,7 +948,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } public restoreViewState(s: editorCommon.IDiffEditorViewState): void { - if (s.original && s.modified) { + if (s && s.original && s.modified) { const diffEditorState = s; this._originalEditor.restoreViewState(diffEditorState.original); this._modifiedEditor.restoreViewState(diffEditorState.modified); @@ -981,6 +1006,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _layoutOverviewRulers(): void { + if (!this._renderOverviewRuler) { + return; + } + if (!this._originalOverviewRuler || !this._modifiedOverviewRuler) { return; } @@ -1091,9 +1120,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } private _updateDecorations(): void { - if (!this._originalEditor.getModel() || !this._modifiedEditor.getModel() || !this._originalOverviewRuler || !this._modifiedOverviewRuler) { + if (!this._originalEditor.getModel() || !this._modifiedEditor.getModel()) { return; } + const lineChanges = (this._diffComputationResult ? this._diffComputationResult.changes : []); const foreignOriginal = this._originalEditorState.getForeignViewZones(this._originalEditor.getWhitespaces()); @@ -1111,15 +1141,15 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } } - private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { - const clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); + private _adjustOptionsForSubEditor(options: Readonly): editorBrowser.IEditorConstructionOptions { + const clonedOptions = { ...options }; + clonedOptions.inDiffEditor = true; clonedOptions.automaticLayout = false; clonedOptions.scrollbar = clonedOptions.scrollbar || {}; clonedOptions.scrollbar.vertical = 'visible'; clonedOptions.folding = false; clonedOptions.codeLens = this._diffCodeLens; clonedOptions.fixedOverflowWidgets = true; - clonedOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode; // clonedOptions.lineDecorationsWidth = '2ch'; if (!clonedOptions.minimap) { clonedOptions.minimap = {}; @@ -1128,37 +1158,38 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return clonedOptions; } - private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { + private _adjustOptionsForLeftHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); - result.inDiffEditor = (this._renderSideBySide ? InDiffEditorState.SideBySideLeft : InDiffEditorState.InlineLeft); if (!this._renderSideBySide) { - // do not wrap hidden editor - result.wordWrap = 'off'; - result.wordWrapMinified = false; - } else if (this._diffWordWrap === 'inherit') { - result.wordWrap = this._wordWrap; - result.wordWrapMinified = this._wordWrapMinified; + // never wrap hidden editor + result.wordWrapOverride1 = 'off'; } else { - result.wordWrap = this._diffWordWrap; - result.wordWrapMinified = this._wordWrapMinified; + result.wordWrapOverride1 = this._diffWordWrap; } result.readOnly = !this._originalIsEditable; result.extraEditorClassName = 'original-in-monaco-diff-editor'; - return result; + return { + ...result, + dimension: { + height: 0, + width: 0 + } + }; } - private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { + private _adjustOptionsForRightHandSide(options: Readonly): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); - result.inDiffEditor = (this._renderSideBySide ? InDiffEditorState.SideBySideRight : InDiffEditorState.InlineRight); - if (this._diffWordWrap === 'inherit') { - result.wordWrap = this._wordWrap; - } else { - result.wordWrap = this._diffWordWrap; - } + result.wordWrapOverride1 = this._diffWordWrap; result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; - return result; + return { + ...result, + dimension: { + height: 0, + width: 0 + } + }; } public doLayout(): void { @@ -1187,7 +1218,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._overviewViewportDomElement.setHeight(30); this._originalEditor.layout({ width: splitPoint, height: (height - reviewHeight) }); - this._modifiedEditor.layout({ width: width - splitPoint - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH, height: (height - reviewHeight) }); + this._modifiedEditor.layout({ width: width - splitPoint - (this._renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), height: (height - reviewHeight) }); if (this._originalOverviewRuler || this._modifiedOverviewRuler) { this._layoutOverviewRulers(); @@ -1241,6 +1272,12 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return (this._elementSizeObserver.getHeight() - this._getReviewHeight()); }, + getOptions: () => { + return { + renderOverviewRuler: this._renderOverviewRuler + }; + }, + getContainerDomNode: () => { return this._containerDomElement; }, @@ -1370,6 +1407,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE interface IDataSource { getWidth(): number; getHeight(): number; + getOptions(): { renderOverviewRuler: boolean; }; getContainerDomNode(): HTMLElement; relayoutEditors(): void; @@ -1841,7 +1879,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IVerti public layout(sashRatio: number | null = this._sashRatio): number { const w = this._dataSource.getWidth(); - const contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; + const contentWidth = w - (this._dataSource.getOptions().renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); let sashPosition = Math.floor((sashRatio || 0.5) * contentWidth); const midPoint = Math.floor(0.5 * contentWidth); @@ -1874,7 +1912,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IVerti private _onSashDrag(e: ISashEvent): void { const w = this._dataSource.getWidth(); - const contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; + const contentWidth = w - (this._dataSource.getOptions().renderOverviewRuler ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); const sashPosition = this.layout((this._startSashPosition! + (e.currentX - e.startX)) / contentWidth); this._sashRatio = sashPosition / contentWidth; @@ -2405,7 +2443,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { const html = sb.build(); const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; - domNode.innerHTML = trustedhtml as unknown as string; + domNode.innerHTML = trustedhtml as string; viewZone.minWidthInPx = (maxCharsPerLine * typicalHalfwidthCharacterWidth); if (viewLineCounts) { diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 06b4303016..30b671a305 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -80,6 +80,8 @@ const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, nls export class DiffReview extends Disposable { + private static _ttPolicy = window.trustedTypes?.createPolicy('diffReview', { createHTML: value => value }); + private readonly _diffEditor: DiffEditorWidget; private _isVisible: boolean; public readonly shadow: FastDomNode; @@ -734,14 +736,18 @@ export class DiffReview extends Disposable { let lineContent: string; if (modifiedLine !== 0) { - cell.insertAdjacentHTML('beforeend', - this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine) - ); + let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); lineContent = modifiedModel.getLineContent(modifiedLine); } else { - cell.insertAdjacentHTML('beforeend', - this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine) - ); + let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); lineContent = originalModel.getLineContent(originalLine); } diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 6df8720a1d..04a678df4c 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -82,7 +82,7 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { @IClipboardService clipboardService: IClipboardService, @IEditorProgressService editorProgressService: IEditorProgressService, ) { - super(domElement, parentEditor.getRawOptions(), clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, parentEditor.getRawOptions(), {}, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 5747fe8e8c..421d8a3d83 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -272,7 +272,7 @@ function migrateOptions(options: IEditorOptions): void { } } -function deepCloneAndMigrateOptions(_options: IEditorOptions): IEditorOptions { +function deepCloneAndMigrateOptions(_options: Readonly): IEditorOptions { const options = objects.deepClone(_options); migrateOptions(options); return options; @@ -298,7 +298,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _readOptions: RawEditorOptions; protected _validatedOptions: ValidatedEditorOptions; - constructor(isSimpleWidget: boolean, _options: IEditorOptions) { + constructor(isSimpleWidget: boolean, _options: Readonly) { super(); this.isSimpleWidget = isSimpleWidget; @@ -318,8 +318,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC public observeReferenceElement(dimension?: IDimension): void { } - public dispose(): void { - super.dispose(); + public updatePixelRatio(): void { } protected _recomputeOptions(): void { @@ -348,7 +347,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _computeInternalOptions(): ComputedEditorOptions { const partialEnv = this._getEnvConfiguration(); - const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, this.isSimpleWidget); + const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, partialEnv.pixelRatio, this.isSimpleWidget); const env: IEnvironmentalOptions = { memory: this._computeOptionsMemory, outerWidth: partialEnv.outerWidth, @@ -394,7 +393,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC return true; } - public updateOptions(_newOptions: IEditorOptions): void { + public updateOptions(_newOptions: Readonly): void { if (typeof _newOptions === 'undefined') { return; } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index f5c1e2a6e0..5c26e09ba0 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -41,14 +41,6 @@ export const enum EditorAutoIndentStrategy { Full = 4 } -export const enum InDiffEditorState { - None = 0, - SideBySideLeft = 1, - SideBySideRight = 2, - InlineLeft = 3, - InlineRight = 4 -} - /** * Configuration options for the editor. */ @@ -56,7 +48,7 @@ export interface IEditorOptions { /** * This editor is used inside a diff editor. */ - inDiffEditor?: InDiffEditorState; + inDiffEditor?: boolean; /** * The aria label for the editor's textarea (when it is focused). */ @@ -272,6 +264,14 @@ export interface IEditorOptions { * Defaults to "off". */ wordWrap?: 'off' | 'on' | 'wordWrapColumn' | 'bounded'; + /** + * Override the `wordWrap` setting. + */ + wordWrapOverride1?: 'off' | 'on' | 'inherit'; + /** + * Override the `wordWrapOverride1` setting. + */ + wordWrapOverride2?: 'off' | 'on' | 'inherit'; /** * Control the wrapping of the editor. * When `wordWrap` = "off", the lines will never wrap. @@ -281,11 +281,6 @@ export interface IEditorOptions { * Defaults to 80. */ wordWrapColumn?: number; - /** - * Force word wrapping when the text appears to be of a minified/generated file. - * Defaults to true. - */ - wordWrapMinified?: boolean; /** * Control indentation of wrapped lines. Can be: 'none', 'same', 'indent' or 'deepIndent'. * Defaults to 'same' in vscode and to 'none' in monaco-editor. @@ -630,6 +625,10 @@ export interface IEditorOptions { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -687,6 +686,11 @@ export interface IDiffEditorOptions extends IEditorOptions { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * Is the diff editor should render overview ruler + * Defaults to true + */ + renderOverviewRuler?: boolean; /** * Control the wrapping of the diff editor. */ @@ -1972,8 +1976,8 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption>; + +class EditorInlineHints extends BaseEditorOption { + + constructor() { + const defaults: EditorInlineHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; + super( + EditorOption.inlineHints, 'inlineHints', defaults, + { + 'editor.inlineHints.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('inlineHints.enable', "Enables the inline hints in the editor.") + }, + 'editor.inlineHints.fontSize': { + type: 'number', + default: defaults.fontSize, + description: nls.localize('inlineHints.fontSize', "Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") + }, + 'editor.inlineHints.fontFamily': { + type: 'string', + default: defaults.fontFamily, + description: nls.localize('inlineHints.fontFamily', "Controls font family of inline hints in the editor.") + }, + } + ); + } + + public validate(_input: any): EditorInlineHintsOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IEditorInlineHintsOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled), + fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), + fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily) + }; + } +} + +//#endregion + //#region lineHeight class EditorLineHeight extends EditorIntOption { @@ -3285,7 +3355,7 @@ class EditorSuggest extends BaseEditorOption 0) { this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index 3d87428c15..750f1d90f6 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -297,6 +297,20 @@ export class CursorMoveCommands { return this._moveDownByModelLines(viewModel, cursors, inSelectionMode, value); } } + case CursorMove.Direction.PrevBlankLine: { + if (unit === CursorMove.Unit.WrappedLine) { + return cursors.map(cursor => CursorState.fromViewState(MoveOperations.moveToPrevBlankLine(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode))); + } else { + return cursors.map(cursor => CursorState.fromModelState(MoveOperations.moveToPrevBlankLine(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode))); + } + } + case CursorMove.Direction.NextBlankLine: { + if (unit === CursorMove.Unit.WrappedLine) { + return cursors.map(cursor => CursorState.fromViewState(MoveOperations.moveToNextBlankLine(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode))); + } else { + return cursors.map(cursor => CursorState.fromModelState(MoveOperations.moveToNextBlankLine(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode))); + } + } case CursorMove.Direction.WrappedLineStart: { // Move to the beginning of the current view line return this._moveToViewMinColumn(viewModel, cursors, inSelectionMode); @@ -412,7 +426,11 @@ export class CursorMoveCommands { const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); let newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - if (skipWrappingPointStop && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { + if (skipWrappingPointStop + && noOfColumns === 1 + && cursor.viewState.position.column === viewModel.getLineMinColumn(cursor.viewState.position.lineNumber) + && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber + ) { // moved over to the previous view line const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) { @@ -445,7 +463,11 @@ export class CursorMoveCommands { const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); let newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - if (skipWrappingPointStop && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { + if (skipWrappingPointStop + && noOfColumns === 1 + && cursor.viewState.position.column === viewModel.getLineMaxColumn(cursor.viewState.position.lineNumber) + && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber + ) { // moved over to the next view line const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) { @@ -606,7 +628,7 @@ export namespace CursorMove { description: `Property-value pairs that can be passed through this argument: * 'to': A mandatory logical position value providing where to move the cursor. \`\`\` - 'left', 'right', 'up', 'down' + 'left', 'right', 'up', 'down', 'prevBlankLine', 'nextBlankLine', 'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter' 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter' 'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside' @@ -625,7 +647,7 @@ export namespace CursorMove { 'properties': { 'to': { 'type': 'string', - 'enum': ['left', 'right', 'up', 'down', 'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter', 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter', 'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside'] + 'enum': ['left', 'right', 'up', 'down', 'prevBlankLine', 'nextBlankLine', 'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter', 'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter', 'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside'] }, 'by': { 'type': 'string', @@ -654,6 +676,9 @@ export namespace CursorMove { Up: 'up', Down: 'down', + PrevBlankLine: 'prevBlankLine', + NextBlankLine: 'nextBlankLine', + WrappedLineStart: 'wrappedLineStart', WrappedLineFirstNonWhitespaceCharacter: 'wrappedLineFirstNonWhitespaceCharacter', WrappedLineColumnCenter: 'wrappedLineColumnCenter', @@ -707,6 +732,12 @@ export namespace CursorMove { case RawDirection.Down: direction = Direction.Down; break; + case RawDirection.PrevBlankLine: + direction = Direction.PrevBlankLine; + break; + case RawDirection.NextBlankLine: + direction = Direction.NextBlankLine; + break; case RawDirection.WrappedLineStart: direction = Direction.WrappedLineStart; break; @@ -782,6 +813,8 @@ export namespace CursorMove { Right, Up, Down, + PrevBlankLine, + NextBlankLine, WrappedLineStart, WrappedLineFirstNonWhitespaceCharacter, @@ -801,6 +834,8 @@ export namespace CursorMove { | Direction.Right | Direction.Up | Direction.Down + | Direction.PrevBlankLine + | Direction.NextBlankLine | Direction.WrappedLineStart | Direction.WrappedLineFirstNonWhitespaceCharacter | Direction.WrappedLineColumnCenter diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index 2c7aef629f..e60c892b52 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -39,11 +39,11 @@ export class MoveOperations { public static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number): Position { const minColumn = model.getLineMinColumn(lineNumber); const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Left); - if (newPosition === -1) { + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Left); + if (newPosition === -1 || newPosition + 1 < minColumn) { return this.leftPosition(model, lineNumber, column); } - return new Position(lineNumber, minColumn + newPosition); + return new Position(lineNumber, newPosition + 1); } public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { @@ -81,13 +81,12 @@ export class MoveOperations { } public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position { - const minColumn = model.getLineMinColumn(lineNumber); const lineContent = model.getLineContent(lineNumber); - const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Right); + const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right); if (newPosition === -1) { return this.rightPosition(model, lineNumber, column); } - return new Position(lineNumber, minColumn + newPosition); + return new Position(lineNumber, newPosition + 1); } public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition { @@ -230,6 +229,47 @@ export class MoveOperations { ); } + private static _isBlankLine(model: ICursorSimpleModel, lineNumber: number): boolean { + if (model.getLineFirstNonWhitespaceColumn(lineNumber) === 0) { + // empty or contains only whitespace + return true; + } + return false; + } + + public static moveToPrevBlankLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState { + let lineNumber = cursor.position.lineNumber; + + // If our current line is blank, move to the previous non-blank line + while (lineNumber > 1 && this._isBlankLine(model, lineNumber)) { + lineNumber--; + } + + // Find the previous blank line + while (lineNumber > 1 && !this._isBlankLine(model, lineNumber)) { + lineNumber--; + } + + return cursor.move(inSelectionMode, lineNumber, model.getLineMinColumn(lineNumber), 0); + } + + public static moveToNextBlankLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState { + const lineCount = model.getLineCount(); + let lineNumber = cursor.position.lineNumber; + + // If our current line is blank, move to the next non-blank line + while (lineNumber < lineCount && this._isBlankLine(model, lineNumber)) { + lineNumber++; + } + + // Find the next blank line + while (lineNumber < lineCount && !this._isBlankLine(model, lineNumber)) { + lineNumber++; + } + + return cursor.move(inSelectionMode, lineNumber, model.getLineMinColumn(lineNumber), 0); + } + public static moveToBeginningOfLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState { let lineNumber = cursor.position.lineNumber; let minColumn = model.getLineMinColumn(lineNumber); diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 12924242cc..3b2e01264d 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -351,13 +351,6 @@ export class TypeOperations { if (ir) { let oldEndViewColumn = CursorColumns.visibleColumnFromColumn2(config, model, range.getEndPosition()); const oldEndColumn = range.endColumn; - - let beforeText = '\n'; - if (indentation !== config.normalizeIndentation(ir.beforeEnter)) { - beforeText = config.normalizeIndentation(ir.beforeEnter) + lineText.substring(indentation.length, range.startColumn - 1) + '\n'; - range = new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn); - } - const newLineContent = model.getLineContent(range.endLineNumber); const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent); if (firstNonWhitespace >= 0) { @@ -367,7 +360,7 @@ export class TypeOperations { } if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, beforeText + config.normalizeIndentation(ir.afterEnter), true); + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); } else { let offset = 0; if (oldEndColumn <= firstNonWhitespace + 1) { @@ -376,7 +369,7 @@ export class TypeOperations { } offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); } - return new ReplaceCommandWithOffsetCursorState(range, beforeText + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); } } } diff --git a/src/vs/editor/common/diff/diffComputer.ts b/src/vs/editor/common/diff/diffComputer.ts index 1ac3802bfc..60b69baef1 100644 --- a/src/vs/editor/common/diff/diffComputer.ts +++ b/src/vs/editor/common/diff/diffComputer.ts @@ -313,6 +313,13 @@ export class DiffComputer { if (this.original.lines.length === 1 && this.original.lines[0].length === 0) { // empty original => fast path + if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) { + return { + quitEarly: false, + changes: [] + }; + } + return { quitEarly: false, changes: [{ diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 43c8c3eb66..8767a11b67 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -157,9 +157,10 @@ export interface IConfiguration extends IDisposable { setMaxLineNumber(maxLineNumber: number): void; setViewLineCount(viewLineCount: number): void; - updateOptions(newOptions: IEditorOptions): void; + updateOptions(newOptions: Readonly): void; getRawOptions(): IEditorOptions; observeReferenceElement(dimension?: IDimension): void; + updatePixelRatio(): void; setIsDominatedByLongLines(isDominatedByLongLines: boolean): void; } @@ -605,6 +606,7 @@ export interface IThemeDecorationRenderOptions { fontStyle?: string; fontWeight?: string; + fontSize?: string; textDecoration?: string; cursor?: string; color?: string | ThemeColor; @@ -629,13 +631,17 @@ export interface IContentDecorationRenderOptions { border?: string; borderColor?: string | ThemeColor; + borderRadius?: string; fontStyle?: string; fontWeight?: string; + fontSize?: string; + fontFamily?: string; textDecoration?: string; color?: string | ThemeColor; backgroundColor?: string | ThemeColor; margin?: string; + padding?: string; width?: string; height?: string; } diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index c7338085a4..7124efe4c2 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -61,6 +61,7 @@ export namespace EditorContextKeys { export const hasReferenceProvider = new RawContextKey('editorHasReferenceProvider', false); export const hasRenameProvider = new RawContextKey('editorHasRenameProvider', false); export const hasSignatureHelpProvider = new RawContextKey('editorHasSignatureHelpProvider', false); + export const hasInlineHintsProvider = new RawContextKey('editorHasInlineHintsProvider', false); // -- mode context keys: formatting export const hasDocumentFormattingProvider = new RawContextKey('editorHasDocumentFormattingProvider', false); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index d2a3f428cd..f5f08cf0f5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -600,12 +600,6 @@ export interface ITextModel { */ setValue(newValue: string): void; - /** - * Replace the entire text buffer value contained in this model. - * @internal - */ - setValueFromTextBuffer(newValue: ITextBuffer): void; - /** * Get the text stored in this model. * @param eol The end of line character preference. Defaults to `EndOfLinePreference.TextDefined`. @@ -855,7 +849,12 @@ export interface ITextModel { /** * @internal */ - hasSemanticTokens(): boolean; + hasCompleteSemanticTokens(): boolean; + + /** + * @internal + */ + hasSomeSemanticTokens(): boolean; /** * Flush all tokenization state. @@ -1094,12 +1093,17 @@ export interface ITextModel { detectIndentation(defaultInsertSpaces: boolean, defaultTabSize: number): void; /** - * Push a stack element onto the undo stack. This acts as an undo/redo point. - * The idea is to use `pushEditOperations` to edit the model and then to - * `pushStackElement` to create an undo/redo stop point. + * Close the current undo-redo element. + * This offers a way to create an undo/redo stop point. */ pushStackElement(): void; + /** + * Open the current undo-redo element. + * This offers a way to remove the current undo/redo stop point. + */ + popStackElement(): void; + /** * Push edit operations, basically editing the model. This is the preferred way * of editing the model. The edit operations will land on the undo stack. @@ -1143,7 +1147,7 @@ export interface ITextModel { _applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void; /** - * Undo edit operations until the first previous stop point created by `pushStackElement`. + * Undo edit operations until the previous undo/redo point. * The inverse edit operations will be pushed on the redo stack. * @internal */ @@ -1156,7 +1160,7 @@ export interface ITextModel { canUndo(): boolean; /** - * Redo edit operations until the next stop point created by `pushStackElement`. + * Redo edit operations until the next undo/redo point. * The inverse edit operations will be pushed on the undo stack. * @internal */ @@ -1266,7 +1270,7 @@ export interface ITextBufferBuilder { * @internal */ export interface ITextBufferFactory { - create(defaultEOL: DefaultEndOfLine): ITextBuffer; + create(defaultEOL: DefaultEndOfLine): { textBuffer: ITextBuffer; disposable: IDisposable; }; getFirstLineText(lengthLimit: number): string; } diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 462093d5b7..cb21989b6d 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -199,6 +199,12 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement { } } + public open(): void { + if (!(this._data instanceof SingleModelEditStackData)) { + this._data = SingleModelEditStackData.deserialize(this._data); + } + } + public undo(): void { if (URI.isUri(this.model)) { // don't have a model @@ -315,6 +321,10 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { this._isOpen = false; } + public open(): void { + // cannot reopen + } + public undo(): void { this._isOpen = false; @@ -386,6 +396,13 @@ export class EditStack { } } + public popStackElement(): void { + const lastElement = this._undoRedoService.getLastElement(this._model.uri); + if (isEditStackElement(lastElement)) { + lastElement.open(); + } + } + public clear(): void { this._undoRedoService.removeElements(this._model.uri); } diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts index 36677ef131..bcf76b61c7 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CharCode } from 'vs/base/common/charCode'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ITextBufferFactory } from 'vs/editor/common/model'; import { StringBuffer, createLineStarts, createLineStartsFast } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; @@ -38,7 +39,7 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { return '\n'; } - public create(defaultEOL: DefaultEndOfLine): ITextBuffer { + public create(defaultEOL: DefaultEndOfLine): { textBuffer: ITextBuffer; disposable: IDisposable; } { const eol = this._getEOL(defaultEOL); let chunks = this._chunks; @@ -54,7 +55,8 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { } } - return new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); + const textBuffer = new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); + return { textBuffer: textBuffer, disposable: textBuffer }; } public getFirstLineText(lengthLimit: number): string { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 3eb95130ad..6be356e869 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -37,6 +37,7 @@ import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; import { Constants } from 'vs/base/common/uint'; +import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -106,7 +107,7 @@ export function createTextBufferFactoryFromSnapshot(snapshot: model.ITextSnapsho return builder.finish(); } -export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): model.ITextBuffer { +export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): { textBuffer: model.ITextBuffer; disposable: IDisposable; } { const factory = (typeof value === 'string' ? createTextBufferFactory(value) : value); return factory.create(defaultEOL); } @@ -268,6 +269,7 @@ export class TextModel extends Disposable implements model.ITextModel { private readonly _undoRedoService: IUndoRedoService; private _attachedEditorCount: number; private _buffer: model.ITextBuffer; + private _bufferDisposable: IDisposable; private _options: model.TextModelResolvedOptions; private _isDisposed: boolean; @@ -328,7 +330,9 @@ export class TextModel extends Disposable implements model.ITextModel { this._undoRedoService = undoRedoService; this._attachedEditorCount = 0; - this._buffer = createTextBuffer(source, creationOptions.defaultEOL); + const { textBuffer, disposable } = createTextBuffer(source, creationOptions.defaultEOL); + this._buffer = textBuffer; + this._bufferDisposable = disposable; this._options = TextModel.resolveOptions(this._buffer, creationOptions); @@ -386,10 +390,13 @@ export class TextModel extends Disposable implements model.ITextModel { this._tokenization.dispose(); this._isDisposed = true; super.dispose(); + this._bufferDisposable.dispose(); this._isDisposing = false; // Manually release reference to previous text buffer to avoid large leaks // in case someone leaks a TextModel reference - this._buffer = createTextBuffer('', this._options.defaultEOL); + const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true); + emptyDisposedTextBuffer.dispose(); + this._buffer = emptyDisposedTextBuffer; } private _assertNotDisposed(): void { @@ -423,8 +430,8 @@ export class TextModel extends Disposable implements model.ITextModel { return; } - const textBuffer = createTextBuffer(value, this._options.defaultEOL); - this.setValueFromTextBuffer(textBuffer); + const { textBuffer, disposable } = createTextBuffer(value, this._options.defaultEOL); + this._setValueFromTextBuffer(textBuffer, disposable); } private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean): IModelContentChangedEvent { @@ -443,18 +450,16 @@ export class TextModel extends Disposable implements model.ITextModel { }; } - public setValueFromTextBuffer(textBuffer: model.ITextBuffer): void { + private _setValueFromTextBuffer(textBuffer: model.ITextBuffer, textBufferDisposable: IDisposable): void { this._assertNotDisposed(); - if (textBuffer === null) { - // There's nothing to do - return; - } const oldFullModelRange = this.getFullModelRange(); const oldModelValueLength = this.getValueLengthInRange(oldFullModelRange); const endLineNumber = this.getLineCount(); const endColumn = this.getLineMaxColumn(endLineNumber); this._buffer = textBuffer; + this._bufferDisposable.dispose(); + this._bufferDisposable = textBufferDisposable; this._increaseVersionId(); // Flush all tokens @@ -1225,6 +1230,10 @@ export class TextModel extends Disposable implements model.ITextModel { this._commandManager.pushStackElement(); } + public popStackElement(): void { + this._commandManager.popStackElement(); + } + public pushEOL(eol: model.EndOfLineSequence): void { const currentEOL = (this.getEOL() === '\n' ? model.EndOfLineSequence.LF : model.EndOfLineSequence.CRLF); if (currentEOL === eol) { @@ -1880,12 +1889,16 @@ export class TextModel extends Disposable implements model.ITextModel { }); } - public hasSemanticTokens(): boolean { + public hasCompleteSemanticTokens(): boolean { return this._tokens2.isComplete(); } + public hasSomeSemanticTokens(): boolean { + return !this._tokens2.isEmpty(); + } + public setPartialSemanticTokens(range: Range, tokens: MultilineTokens2[]): void { - if (this.hasSemanticTokens()) { + if (this.hasCompleteSemanticTokens()) { return; } const changedRange = this._tokens2.setPartial(range, tokens); diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index a51075294a..1d6b4ad5f6 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -359,7 +359,7 @@ export class TextModelTokenization extends Disposable { const text = this._textModel.getLineContent(lineIndex + 1); const lineStartState = this._tokenizationStateStore.getBeginState(lineIndex); - const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, lineStartState!); + const r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, lineStartState!); builder.add(lineIndex + 1, r.tokens); this._tokenizationStateStore.setEndState(linesLength, lineIndex, r.endState); lineIndex = this._tokenizationStateStore.invalidLineStartIndex - 1; // -1 because the outer loop increments it @@ -410,13 +410,13 @@ export class TextModelTokenization extends Disposable { const languageIdentifier = this._textModel.getLanguageIdentifier(); let state = initialState; for (let i = fakeLines.length - 1; i >= 0; i--) { - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], state); + let r = safeTokenize(languageIdentifier, this._tokenizationSupport, fakeLines[i], false, state); state = r.endState; } for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { let text = this._textModel.getLineContent(lineNumber); - let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, state); + let r = safeTokenize(languageIdentifier, this._tokenizationSupport, text, true, state); builder.add(lineNumber, r.tokens); this._tokenizationStateStore.setFakeTokens(lineNumber - 1); state = r.endState; @@ -443,12 +443,12 @@ function initializeTokenization(textModel: TextModel): [ITokenizationSupport | n return [tokenizationSupport, initialState]; } -function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, state: IState): TokenizationResult2 { +function safeTokenize(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport | null, text: string, hasEOL: boolean, state: IState): TokenizationResult2 { let r: TokenizationResult2 | null = null; if (tokenizationSupport) { try { - r = tokenizationSupport.tokenize2(text, state.clone(), 0); + r = tokenizationSupport.tokenize2(text, hasEOL, state.clone(), 0); } catch (e) { onUnexpectedError(e); } diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index bc8b169cc0..193ca7ada4 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -884,6 +884,10 @@ export class TokensStore2 { this._isComplete = false; } + public isEmpty(): boolean { + return (this._pieces.length === 0); + } + public set(pieces: MultilineTokens2[] | null, isComplete: boolean): void { this._pieces = pieces || []; this._isComplete = isComplete; diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index e5e3799bc2..dc8bd42954 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -211,9 +211,9 @@ export interface ITokenizationSupport { getInitialState(): IState; // add offsetDelta to each of the returned indices - tokenize(line: string, state: IState, offsetDelta: number): TokenizationResult; + tokenize(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult; - tokenize2(line: string, state: IState, offsetDelta: number): TokenizationResult2; + tokenize2(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult2; } /** @@ -1659,6 +1659,19 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } +export interface InlineHint { + text: string; + range: IRange; + description?: string | IMarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface InlineHintsProvider { + onDidChangeInlineHints?: Event | undefined; + provideInlineHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; +} + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -1764,6 +1777,11 @@ export const TypeDefinitionProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const InlineHintsProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ @@ -1876,3 +1894,14 @@ export interface ITokenizationRegistry { * @internal */ export const TokenizationRegistry = new TokenizationRegistryImpl(); + + +/** + * @internal + */ +export enum ExternalUriOpenerPriority { + None = 0, + Option = 1, + Default = 2, + Preferred = 3, +} diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index ede9f1cdaa..4ae7935d0c 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -150,7 +150,7 @@ export interface OnEnterRule { /** * This rule will only execute if the text above the this line matches this regular expression. */ - oneLineAboveText?: RegExp; + previousLineText?: RegExp; /** * The action to execute. */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index 796288a9f0..22ab9e5675 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -57,33 +57,21 @@ export class RichEditSupport { public readonly indentationRules: IndentationRule | undefined; public readonly foldingRules: FoldingRules; - constructor(languageIdentifier: LanguageIdentifier, previous: RichEditSupport | undefined, rawConf: LanguageConfiguration) { + constructor(languageIdentifier: LanguageIdentifier, rawConf: LanguageConfiguration) { this._languageIdentifier = languageIdentifier; - this._brackets = null; this._electricCharacter = null; - - let prev: LanguageConfiguration | null = null; - if (previous) { - prev = previous._conf; - } - - this._conf = RichEditSupport._mergeConf(prev, rawConf); - + this._conf = rawConf; this._onEnterSupport = (this._conf.brackets || this._conf.indentationRules || this._conf.onEnterRules ? new OnEnterSupport(this._conf) : null); this.comments = RichEditSupport._handleComments(this._conf); - this.characterPair = new CharacterPairSupport(this._conf); - this.wordDefinition = this._conf.wordPattern || DEFAULT_WORD_REGEXP; - this.indentationRules = this._conf.indentationRules; if (this._conf.indentationRules) { this.indentRulesSupport = new IndentRulesSupport(this._conf.indentationRules); } else { this.indentRulesSupport = null; } - this.foldingRules = this._conf.folding || {}; } @@ -101,26 +89,11 @@ export class RichEditSupport { return this._electricCharacter; } - public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { + public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { if (!this._onEnterSupport) { return null; } - return this._onEnterSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText); - } - - private static _mergeConf(prev: LanguageConfiguration | null, current: LanguageConfiguration): LanguageConfiguration { - return { - comments: (prev ? current.comments || prev.comments : current.comments), - brackets: (prev ? current.brackets || prev.brackets : current.brackets), - wordPattern: (prev ? current.wordPattern || prev.wordPattern : current.wordPattern), - indentationRules: (prev ? current.indentationRules || prev.indentationRules : current.indentationRules), - onEnterRules: (prev ? current.onEnterRules || prev.onEnterRules : current.onEnterRules), - autoClosingPairs: (prev ? current.autoClosingPairs || prev.autoClosingPairs : current.autoClosingPairs), - surroundingPairs: (prev ? current.surroundingPairs || prev.surroundingPairs : current.surroundingPairs), - autoCloseBefore: (prev ? current.autoCloseBefore || prev.autoCloseBefore : current.autoCloseBefore), - folding: (prev ? current.folding || prev.folding : current.folding), - __electricCharacterSupport: (prev ? current.__electricCharacterSupport || prev.__electricCharacterSupport : current.__electricCharacterSupport), - }; + return this._onEnterSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); } private static _handleComments(conf: LanguageConfiguration): ICommentsConfiguration | null { @@ -151,38 +124,120 @@ export class LanguageConfigurationChangeEvent { ) { } } -export class LanguageConfigurationRegistryImpl { +class LanguageConfigurationEntry { - private readonly _entries = new Map(); + constructor( + public readonly configuration: LanguageConfiguration, + public readonly priority: number, + public readonly order: number + ) { } - private readonly _onDidChange = new Emitter(); - public readonly onDidChange: Event = this._onDidChange.event; + public static cmp(a: LanguageConfigurationEntry, b: LanguageConfigurationEntry) { + if (a.priority === b.priority) { + // higher order last + return a.order - b.order; + } + // higher priority last + return a.priority - b.priority; + } +} - public register(languageIdentifier: LanguageIdentifier, configuration: LanguageConfiguration): IDisposable { - let previous = this._getRichEditSupport(languageIdentifier.id); - let current = new RichEditSupport(languageIdentifier, previous, configuration); - this._entries.set(languageIdentifier.id, current); - this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageIdentifier)); +class LanguageConfigurationEntries { + + private readonly _entries: LanguageConfigurationEntry[]; + private _order: number; + private _resolved: RichEditSupport | null = null; + + constructor( + public readonly languageIdentifier: LanguageIdentifier + ) { + this._entries = []; + this._order = 0; + this._resolved = null; + } + + public register(configuration: LanguageConfiguration, priority: number): IDisposable { + const entry = new LanguageConfigurationEntry(configuration, priority, ++this._order); + this._entries.push(entry); + this._resolved = null; return toDisposable(() => { - if (this._entries.get(languageIdentifier.id) === current) { - this._entries.set(languageIdentifier.id, previous); - this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageIdentifier)); + for (let i = 0; i < this._entries.length; i++) { + if (this._entries[i] === entry) { + this._entries.splice(i, 1); + this._resolved = null; + break; + } } }); } - private _getRichEditSupport(languageId: LanguageId): RichEditSupport | undefined { - return this._entries.get(languageId); + public getRichEditSupport(): RichEditSupport | null { + if (!this._resolved) { + const config = this._resolve(); + if (config) { + this._resolved = new RichEditSupport(this.languageIdentifier, config); + } + } + return this._resolved; } - public getIndentationRules(languageId: LanguageId) { - const value = this._entries.get(languageId); - - if (!value) { + private _resolve(): LanguageConfiguration | null { + if (this._entries.length === 0) { return null; } + this._entries.sort(LanguageConfigurationEntry.cmp); + const result: LanguageConfiguration = {}; + for (const entry of this._entries) { + const conf = entry.configuration; + result.comments = conf.comments || result.comments; + result.brackets = conf.brackets || result.brackets; + result.wordPattern = conf.wordPattern || result.wordPattern; + result.indentationRules = conf.indentationRules || result.indentationRules; + result.onEnterRules = conf.onEnterRules || result.onEnterRules; + result.autoClosingPairs = conf.autoClosingPairs || result.autoClosingPairs; + result.surroundingPairs = conf.surroundingPairs || result.surroundingPairs; + result.autoCloseBefore = conf.autoCloseBefore || result.autoCloseBefore; + result.folding = conf.folding || result.folding; + result.__electricCharacterSupport = conf.__electricCharacterSupport || result.__electricCharacterSupport; + } + return result; + } +} - return value.indentationRules || null; +export class LanguageConfigurationRegistryImpl { + + private readonly _entries2 = new Map(); + + private readonly _onDidChange = new Emitter(); + public readonly onDidChange: Event = this._onDidChange.event; + + /** + * @param priority Use a higher number for higher priority + */ + public register(languageIdentifier: LanguageIdentifier, configuration: LanguageConfiguration, priority: number = 0): IDisposable { + let entries = this._entries2.get(languageIdentifier.id); + if (!entries) { + entries = new LanguageConfigurationEntries(languageIdentifier); + this._entries2.set(languageIdentifier.id, entries); + } + + const disposable = entries.register(configuration, priority); + this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageIdentifier)); + + return toDisposable(() => { + disposable.dispose(); + this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageIdentifier)); + }); + } + + private _getRichEditSupport(languageId: LanguageId): RichEditSupport | null { + const entries = this._entries2.get(languageId); + return entries ? entries.getRichEditSupport() : null; + } + + public getIndentationRules(languageId: LanguageId): IndentationRule | null { + const value = this._getRichEditSupport(languageId); + return value ? value.indentationRules || null : null; } // begin electricCharacter @@ -273,11 +328,12 @@ export class LanguageConfigurationRegistryImpl { public getWordDefinitions(): [LanguageId, RegExp][] { let result: [LanguageId, RegExp][] = []; - this._entries.forEach((value, language) => { + for (const [language, entries] of this._entries2) { + const value = entries.getRichEditSupport(); if (value) { result.push([language, value.wordDefinition]); } - }); + } return result; } @@ -700,17 +756,17 @@ export class LanguageConfigurationRegistryImpl { afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset); } - let oneLineAboveText = ''; + let previousLineText = ''; if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) { // This is not the first line and the entire line belongs to this mode const oneLineAboveScopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber - 1); if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) { // The line above ends with text belonging to the same mode - oneLineAboveText = oneLineAboveScopedLineTokens.getLineContent(); + previousLineText = oneLineAboveScopedLineTokens.getLineContent(); } } - const enterResult = richEditSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText); + const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText); if (!enterResult) { return null; } @@ -729,6 +785,8 @@ export class LanguageConfigurationRegistryImpl { } else { appendText = ''; } + } else if (indentAction === IndentAction.Indent) { + appendText = '\t' + appendText; } let indentation = this.getIndentationAtPosition(model, range.startLineNumber, range.startColumn); diff --git a/src/vs/editor/common/modes/modesRegistry.ts b/src/vs/editor/common/modes/modesRegistry.ts index 6f22743c41..845273e471 100644 --- a/src/vs/editor/common/modes/modesRegistry.ts +++ b/src/vs/editor/common/modes/modesRegistry.ts @@ -84,4 +84,4 @@ LanguageConfigurationRegistry.register(PLAINTEXT_LANGUAGE_IDENTIFIER, { folding: { offSide: true } -}); +}, 0); diff --git a/src/vs/editor/common/modes/supports/onEnter.ts b/src/vs/editor/common/modes/supports/onEnter.ts index d317c77a52..6edd37e29d 100644 --- a/src/vs/editor/common/modes/supports/onEnter.ts +++ b/src/vs/editor/common/modes/supports/onEnter.ts @@ -49,7 +49,7 @@ export class OnEnterSupport { this._regExpRules = opts.onEnterRules || []; } - public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { + public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null { // (1): `regExpRules` if (autoIndent >= EditorAutoIndentStrategy.Advanced) { for (let i = 0, len = this._regExpRules.length; i < len; i++) { @@ -61,8 +61,8 @@ export class OnEnterSupport { reg: rule.afterText, text: afterEnterText }, { - reg: rule.oneLineAboveText, - text: oneLineAboveText + reg: rule.previousLineText, + text: previousLineText }].every((obj): boolean => { return obj.reg ? obj.reg.test(obj.text) : true; }); diff --git a/src/vs/editor/common/modes/textToHtmlTokenizer.ts b/src/vs/editor/common/modes/textToHtmlTokenizer.ts index ebb6300400..25e229590a 100644 --- a/src/vs/editor/common/modes/textToHtmlTokenizer.ts +++ b/src/vs/editor/common/modes/textToHtmlTokenizer.ts @@ -12,12 +12,12 @@ import { NULL_STATE, nullTokenize2 } from 'vs/editor/common/modes/nullMode'; export interface IReducedTokenizationSupport { getInitialState(): IState; - tokenize2(line: string, state: IState, offsetDelta: number): TokenizationResult2; + tokenize2(line: string, hasEOL: boolean, state: IState, offsetDelta: number): TokenizationResult2; } const fallback: IReducedTokenizationSupport = { getInitialState: () => NULL_STATE, - tokenize2: (buffer: string, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) + tokenize2: (buffer: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(LanguageId.Null, buffer, state, deltaOffset) }; export function tokenizeToString(text: string, tokenizationSupport: IReducedTokenizationSupport = fallback): string { @@ -110,7 +110,7 @@ function _tokenizeToString(text: string, tokenizationSupport: IReducedTokenizati result += `
`; } - let tokenizationResult = tokenizationSupport.tokenize2(line, currentState, 0); + let tokenizationResult = tokenizationSupport.tokenize2(line, true, currentState, 0); LineTokens.convertToEndOffset(tokenizationResult.tokens, line.length); let lineTokens = new LineTokens(tokenizationResult.tokens, line); let viewLineTokens = lineTokens.inflate(); diff --git a/src/vs/editor/common/services/getSemanticTokens.ts b/src/vs/editor/common/services/getSemanticTokens.ts new file mode 100644 index 0000000000..2e1d06c3d7 --- /dev/null +++ b/src/vs/editor/common/services/getSemanticTokens.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend, DocumentRangeSemanticTokensProviderRegistry, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { assertType } from 'vs/base/common/types'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; +import { Range } from 'vs/editor/common/core/range'; + +export function isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { + return v && !!((v).data); +} + +export function isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { + return v && Array.isArray((v).edits); +} + +export interface IDocumentSemanticTokensResult { + provider: DocumentSemanticTokensProvider; + request: Promise; +} + +export function getDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): IDocumentSemanticTokensResult | null { + const provider = _getDocumentSemanticTokensProvider(model); + if (!provider) { + return null; + } + return { + provider: provider, + request: Promise.resolve(provider.provideDocumentSemanticTokens(model, lastResultId, token)) + }; +} + +function _getDocumentSemanticTokensProvider(model: ITextModel): DocumentSemanticTokensProvider | null { + const result = DocumentSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); +} + +export function getDocumentRangeSemanticTokensProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { + const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); + return (result.length > 0 ? result[0] : null); +} + +CommandsRegistry.registerCommand('_provideDocumentSemanticTokensLegend', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = _getDocumentSemanticTokensProvider(model); + if (!provider) { + // there is no provider => fall back to a document range semantic tokens provider + return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokensLegend', uri); + } + + return provider.getLegend(); +}); + +CommandsRegistry.registerCommand('_provideDocumentSemanticTokens', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const r = getDocumentSemanticTokens(model, null, CancellationToken.None); + if (!r) { + // there is no provider => fall back to a document range semantic tokens provider + return accessor.get(ICommandService).executeCommand('_provideDocumentRangeSemanticTokens', uri, model.getFullModelRange()); + } + + const { provider, request } = r; + + let result: SemanticTokens | SemanticTokensEdits | null | undefined; + try { + result = await request; + } catch (err) { + onUnexpectedExternalError(err); + return undefined; + } + + if (!result || !isSemanticTokens(result)) { + return undefined; + } + + const buff = encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: result.data + }); + if (result.resultId) { + provider.releaseDocumentSemanticTokens(result.resultId); + } + return buff; +}); + +CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokensLegend', async (accessor, ...args): Promise => { + const [uri] = args; + assertType(uri instanceof URI); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = getDocumentRangeSemanticTokensProvider(model); + if (!provider) { + return undefined; + } + + return provider.getLegend(); +}); + +CommandsRegistry.registerCommand('_provideDocumentRangeSemanticTokens', async (accessor, ...args): Promise => { + const [uri, range] = args; + assertType(uri instanceof URI); + assertType(Range.isIRange(range)); + + const model = accessor.get(IModelService).getModel(uri); + if (!model) { + return undefined; + } + + const provider = getDocumentRangeSemanticTokensProvider(model); + if (!provider) { + // there is no provider + return undefined; + } + + let result: SemanticTokens | null | undefined; + try { + result = await provider.provideDocumentRangeSemanticTokens(model, Range.lift(range), CancellationToken.None); + } catch (err) { + onUnexpectedExternalError(err); + return undefined; + } + + if (!result || !isSemanticTokens(result)) { + return undefined; + } + + return encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: result.data + }); +}); diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index c7db9b5755..66ce134078 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -87,13 +87,13 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor this._markerDecorations.clear(); } - getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null { - const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); + getMarker(uri: URI, decoration: IModelDecoration): IMarker | null { + const markerDecorations = this._markerDecorations.get(MODEL_ID(uri)); return markerDecorations ? (markerDecorations.getMarker(decoration) || null) : null; } - getLiveMarkers(model: ITextModel): [Range, IMarker][] { - const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri)); + getLiveMarkers(uri: URI): [Range, IMarker][] { + const markerDecorations = this._markerDecorations.get(MODEL_ID(uri)); return markerDecorations ? markerDecorations.getMarkers() : []; } diff --git a/src/vs/editor/common/services/markersDecorationService.ts b/src/vs/editor/common/services/markersDecorationService.ts index 866e4dccd8..6f31461470 100644 --- a/src/vs/editor/common/services/markersDecorationService.ts +++ b/src/vs/editor/common/services/markersDecorationService.ts @@ -8,6 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IMarker } from 'vs/platform/markers/common/markers'; import { Event } from 'vs/base/common/event'; import { Range } from 'vs/editor/common/core/range'; +import { URI } from 'vs/base/common/uri'; export const IMarkerDecorationsService = createDecorator('markerDecorationsService'); @@ -16,7 +17,7 @@ export interface IMarkerDecorationsService { onDidChangeMarker: Event; - getMarker(model: ITextModel, decoration: IModelDecoration): IMarker | null; + getMarker(uri: URI, decoration: IModelDecoration): IMarker | null; - getLiveMarkers(model: ITextModel): [Range, IMarker][]; + getLiveMarkers(uri: URI): [Range, IMarker][]; } diff --git a/src/vs/editor/common/services/modeService.ts b/src/vs/editor/common/services/modeService.ts index 2ae63c6116..1b8262e113 100644 --- a/src/vs/editor/common/services/modeService.ts +++ b/src/vs/editor/common/services/modeService.ts @@ -50,7 +50,7 @@ export interface IModeService { // --- instantiation create(commaSeparatedMimetypesOrCommaSeparatedIds: string | undefined): ILanguageSelection; createByLanguageName(languageName: string): ILanguageSelection; - createByFilepathOrFirstLine(rsource: URI | null, firstLine?: string): ILanguageSelection; + createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection; triggerMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): void; } diff --git a/src/vs/editor/common/services/modeServiceImpl.ts b/src/vs/editor/common/services/modeServiceImpl.ts index 94fa8fc874..0b08eb915c 100644 --- a/src/vs/editor/common/services/modeServiceImpl.ts +++ b/src/vs/editor/common/services/modeServiceImpl.ts @@ -40,23 +40,24 @@ class LanguageSelection extends Disposable implements ILanguageSelection { } } -export class ModeServiceImpl implements IModeService { +export class ModeServiceImpl extends Disposable implements IModeService { public _serviceBrand: undefined; private readonly _instantiatedModes: { [modeId: string]: IMode; }; private readonly _registry: LanguagesRegistry; - private readonly _onDidCreateMode = new Emitter(); + private readonly _onDidCreateMode = this._register(new Emitter()); public readonly onDidCreateMode: Event = this._onDidCreateMode.event; - protected readonly _onLanguagesMaybeChanged = new Emitter(); + protected readonly _onLanguagesMaybeChanged = this._register(new Emitter()); public readonly onLanguagesMaybeChanged: Event = this._onLanguagesMaybeChanged.event; constructor(warnOnOverwrite = false) { + super(); this._instantiatedModes = {}; - this._registry = new LanguagesRegistry(true, warnOnOverwrite); - this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire()); + this._registry = this._register(new LanguagesRegistry(true, warnOnOverwrite)); + this._register(this._registry.onDidChange(() => this._onLanguagesMaybeChanged.fire())); } protected _onReady(): Promise { diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 5c0407bdfc..31dcab7d4d 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -29,6 +29,7 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { EditStackElement, isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { getDocumentSemanticTokens, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; export interface IEditorSemanticHighlightingOptions { enabled: true | false | 'configuredByTheme'; @@ -412,10 +413,11 @@ export class ModelServiceImpl extends Disposable implements IModelService { public updateModel(model: ITextModel, value: string | ITextBufferFactory): void { const options = this.getCreationOptions(model.getLanguageIdentifier().language, model.uri, model.isForSimpleWidget); - const textBuffer = createTextBuffer(value, options.defaultEOL); + const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL); // Return early if the text is already set in that form if (model.equalsTextBuffer(textBuffer)) { + disposable.dispose(); return; } @@ -428,6 +430,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { () => [] ); model.pushStackElement(); + disposable.dispose(); } private static _commonPrefix(a: ILineSequence, aLen: number, aDelta: number, b: ILineSequence, bLen: number, bDelta: number): number { @@ -513,58 +516,6 @@ export class ModelServiceImpl extends Disposable implements IModelService { if (!modelData) { return; } - const model = modelData.model; - const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); - let maintainUndoRedoStack = false; - let heapSize = 0; - if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(resource))) { - const elements = this._undoRedoService.getElements(resource); - if (elements.past.length > 0 || elements.future.length > 0) { - for (const element of elements.past) { - if (isEditStackElement(element) && element.matchesResource(resource)) { - maintainUndoRedoStack = true; - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance - } - } - for (const element of elements.future) { - if (isEditStackElement(element) && element.matchesResource(resource)) { - maintainUndoRedoStack = true; - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance - } - } - } - } - - if (!maintainUndoRedoStack) { - if (!sharesUndoRedoStack) { - const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); - if (initialUndoRedoSnapshot !== null) { - this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); - } - } - modelData.model.dispose(); - return; - } - - const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; - if (!sharesUndoRedoStack && heapSize > maxMemory) { - // the undo stack for this file would never fit in the configured memory, so don't bother with it. - const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); - if (initialUndoRedoSnapshot !== null) { - this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); - } - modelData.model.dispose(); - return; - } - - this._ensureDisposedModelsHeapSize(maxMemory - heapSize); - - // We only invalidate the elements, but they remain in the undo-redo service. - this._undoRedoService.setElementsValidFlag(resource, false, (element) => (isEditStackElement(element) && element.matchesResource(resource))); - this._insertDisposedModel(new DisposedModelInfo(resource, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); - modelData.model.dispose(); } @@ -599,6 +550,50 @@ export class ModelServiceImpl extends Disposable implements IModelService { const modelId = MODEL_ID(model.uri); const modelData = this._models[modelId]; + const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); + let maintainUndoRedoStack = false; + let heapSize = 0; + if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && schemaShouldMaintainUndoRedoElements(model.uri))) { + const elements = this._undoRedoService.getElements(model.uri); + if (elements.past.length > 0 || elements.future.length > 0) { + for (const element of elements.past) { + if (isEditStackElement(element) && element.matchesResource(model.uri)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(model.uri); + element.setModel(model.uri); // remove reference from text buffer instance + } + } + for (const element of elements.future) { + if (isEditStackElement(element) && element.matchesResource(model.uri)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(model.uri); + element.setModel(model.uri); // remove reference from text buffer instance + } + } + } + } + + const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; + if (!maintainUndoRedoStack) { + if (!sharesUndoRedoStack) { + const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); + if (initialUndoRedoSnapshot !== null) { + this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); + } + } + } else if (!sharesUndoRedoStack && heapSize > maxMemory) { + // the undo stack for this file would never fit in the configured memory, so don't bother with it. + const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot(); + if (initialUndoRedoSnapshot !== null) { + this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot); + } + } else { + this._ensureDisposedModelsHeapSize(maxMemory - heapSize); + // We only invalidate the elements, but they remain in the undo-redo service. + this._undoRedoService.setElementsValidFlag(model.uri, false, (element) => (isEditStackElement(element) && element.matchesResource(model.uri))); + this._insertDisposedModel(new DisposedModelInfo(model.uri, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); + } + delete this._models[modelId]; modelData.dispose(); @@ -718,7 +713,9 @@ class SemanticTokensResponse { } } -class ModelSemanticColoring extends Disposable { +export class ModelSemanticColoring extends Disposable { + + public static FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = 300; private _isDisposed: boolean; private readonly _model: ITextModel; @@ -734,7 +731,7 @@ class ModelSemanticColoring extends Disposable { this._isDisposed = false; this._model = model; this._semanticStyling = stylingProvider; - this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), 300)); + this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY)); this._currentDocumentResponse = null; this._currentDocumentRequestCancellationTokenSource = null; this._documentProvidersChangeListeners = []; @@ -788,11 +785,21 @@ class ModelSemanticColoring extends Disposable { // there is already a request running, let it finish... return; } - const provider = this._getSemanticColoringProvider(); - if (!provider) { + + const cancellationTokenSource = new CancellationTokenSource(); + const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; + const r = getDocumentSemanticTokens(this._model, lastResultId, cancellationTokenSource.token); + if (!r) { + // there is no provider + if (this._currentDocumentResponse) { + // there are semantic tokens set + this._model.setSemanticTokens(null, false); + } return; } - this._currentDocumentRequestCancellationTokenSource = new CancellationTokenSource(); + + const { provider, request } = r; + this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource; const pendingChanges: IModelContentChangedEvent[] = []; const contentChangeListener = this._model.onDidChangeContent((e) => { @@ -801,15 +808,13 @@ class ModelSemanticColoring extends Disposable { const styling = this._semanticStyling.get(provider); - const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; - const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentDocumentRequestCancellationTokenSource.token)); - request.then((res) => { this._currentDocumentRequestCancellationTokenSource = null; contentChangeListener.dispose(); this._setDocumentSemanticTokens(provider, res || null, styling, pendingChanges); }, (err) => { - if (!err || typeof err.message !== 'string' || err.message.indexOf('busy') === -1) { + const isExpectedError = err && (errors.isPromiseCanceledError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1)); + if (!isExpectedError) { errors.onUnexpectedError(err); } @@ -827,14 +832,6 @@ class ModelSemanticColoring extends Disposable { }); } - private static _isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { - return v && !!((v).data); - } - - private static _isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { - return v && Array.isArray((v).edits); - } - private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { for (let i = 0; i < length; i++) { dest[destOffset + i] = src[srcOffset + i]; @@ -843,6 +840,12 @@ class ModelSemanticColoring extends Disposable { private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { const currentResponse = this._currentDocumentResponse; + const rescheduleIfNeeded = () => { + if (pendingChanges.length > 0 && !this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(); + } + }; + if (this._currentDocumentResponse) { this._currentDocumentResponse.dispose(); this._currentDocumentResponse = null; @@ -860,10 +863,11 @@ class ModelSemanticColoring extends Disposable { } if (!tokens) { this._model.setSemanticTokens(null, true); + rescheduleIfNeeded(); return; } - if (ModelSemanticColoring._isSemanticTokensEdits(tokens)) { + if (isSemanticTokensEdits(tokens)) { if (!currentResponse) { // not possible! this._model.setSemanticTokens(null, true); @@ -914,7 +918,7 @@ class ModelSemanticColoring extends Disposable { } } - if (ModelSemanticColoring._isSemanticTokens(tokens)) { + if (isSemanticTokens(tokens)) { this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); @@ -933,21 +937,13 @@ class ModelSemanticColoring extends Disposable { } } } - - if (!this._fetchDocumentSemanticTokens.isScheduled()) { - this._fetchDocumentSemanticTokens.schedule(); - } } this._model.setSemanticTokens(result, true); - return; + } else { + this._model.setSemanticTokens(null, true); } - this._model.setSemanticTokens(null, true); - } - - private _getSemanticColoringProvider(): DocumentSemanticTokensProvider | null { - const result = DocumentSemanticTokensProviderRegistry.ordered(this._model); - return (result.length > 0 ? result[0] : null); + rescheduleIfNeeded(); } } diff --git a/src/vs/workbench/api/common/shared/semanticTokensDto.ts b/src/vs/editor/common/services/semanticTokensDto.ts similarity index 100% rename from src/vs/workbench/api/common/shared/semanticTokensDto.ts rename to src/vs/editor/common/services/semanticTokensDto.ts diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 983583783b..545bc5bdfe 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -282,15 +282,17 @@ export enum EditorOption { wordWrapBreakAfterCharacters = 112, wordWrapBreakBeforeCharacters = 113, wordWrapColumn = 114, - wordWrapMinified = 115, - wrappingIndent = 116, - wrappingStrategy = 117, - showDeprecated = 118, - editorClassName = 119, - pixelRatio = 120, - tabFocusMode = 121, - layoutInfo = 122, - wrappingInfo = 123 + wordWrapOverride1 = 115, + wordWrapOverride2 = 116, + wrappingIndent = 117, + wrappingStrategy = 118, + showDeprecated = 119, + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } /** @@ -325,14 +327,6 @@ export enum EndOfLineSequence { CRLF = 1 } -export enum InDiffEditorState { - None = 0, - SideBySideLeft = 1, - SideBySideRight = 2, - InlineLeft = 3, - InlineRight = 4 -} - /** * Describes what to do with the indentation when pressing Enter. */ diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 43d3490808..10c1a7c0c0 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -11,6 +11,8 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export const enum ViewEventType { + ViewCompositionStart, + ViewCompositionEnd, ViewConfigurationChanged, ViewCursorStateChanged, ViewDecorationsChanged, @@ -29,6 +31,16 @@ export const enum ViewEventType { ViewZonesChanged, } +export class ViewCompositionStartEvent { + public readonly type = ViewEventType.ViewCompositionStart; + constructor() { } +} + +export class ViewCompositionEndEvent { + public readonly type = ViewEventType.ViewCompositionEnd; + constructor() { } +} + export class ViewConfigurationChangedEvent { public readonly type = ViewEventType.ViewConfigurationChanged; @@ -285,7 +297,9 @@ export class ViewZonesChangedEvent { } export type ViewEvent = ( - ViewConfigurationChangedEvent + ViewCompositionStartEvent + | ViewCompositionEndEvent + | ViewConfigurationChangedEvent | ViewCursorStateChangedEvent | ViewDecorationsChangedEvent | ViewFlushedEvent diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index b7dd389fc3..7521bcadcb 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -255,7 +255,7 @@ export class ViewLayout extends Disposable implements IViewLayout { let result = this._linesLayout.getLinesTotalHeight(); if (options.get(EditorOption.scrollBeyondLastLine)) { - result += height - options.get(EditorOption.lineHeight); + result += Math.max(0, height - options.get(EditorOption.lineHeight) - options.get(EditorOption.padding).bottom); } else { result += this._getHorizontalScrollbarHeight(width, contentWidth); } diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 3781e3ff2f..ae4ff72b97 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -303,6 +303,19 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn; } + if (breakOffset <= lastBreakingOffset) { + // Make sure that we are advancing (at least one character) + const charCode = lineText.charCodeAt(lastBreakingOffset); + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + breakOffset = lastBreakingOffset + 2; + breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + 2; + } else { + breakOffset = lastBreakingOffset + 1; + breakOffsetVisibleColumn = lastBreakingOffsetVisibleColumn + computeCharWidth(charCode, lastBreakingOffsetVisibleColumn, tabSize, columnsForFullWidthChar); + } + } + lastBreakingOffset = breakOffset; breakingOffsets[breakingOffsetsCount] = breakOffset; lastBreakingOffsetVisibleColumn = breakOffsetVisibleColumn; @@ -435,6 +448,10 @@ function computeCharWidth(charCode: number, visibleColumn: number, tabSize: numb if (strings.isFullWidthCharacter(charCode)) { return columnsForFullWidthChar; } + if (charCode < 32) { + // when using `editor.renderControlCharacters`, the substitutions are often wide + return columnsForFullWidthChar; + } return 1; } diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index fede1c7790..0c6de95d1e 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -503,15 +503,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return null; } - let hiddenAreas = this.getHiddenAreas(); - let isInHiddenArea = false; - let testPosition = new Position(fromLineNumber, 1); - for (const hiddenArea of hiddenAreas) { - if (hiddenArea.containsPosition(testPosition)) { - isInHiddenArea = true; - break; - } - } + // cannot use this.getHiddenAreas() because those decorations have already seen the effect of this model change + const isInHiddenArea = (fromLineNumber > 2 && !this.lines[fromLineNumber - 2].isVisible()); let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index 0dfe991305..99fb20cb71 100644 --- a/src/vs/editor/common/viewModel/viewEventHandler.ts +++ b/src/vs/editor/common/viewModel/viewEventHandler.ts @@ -33,10 +33,15 @@ export class ViewEventHandler extends Disposable { // --- begin event handlers + public onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { + return false; + } + public onCompositionEnd(e: viewEvents.ViewCompositionEndEvent): boolean { + return false; + } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return false; } - public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return false; } @@ -94,6 +99,18 @@ export class ViewEventHandler extends Disposable { switch (e.type) { + case viewEvents.ViewEventType.ViewCompositionStart: + if (this.onCompositionStart(e)) { + shouldRender = true; + } + break; + + case viewEvents.ViewEventType.ViewCompositionEnd: + if (this.onCompositionEnd(e)) { + shouldRender = true; + } + break; + case viewEvents.ViewEventType.ViewConfigurationChanged: if (this.onConfigurationChanged(e)) { shouldRender = true; diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 1b749328d4..203cd8ee7d 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -164,6 +164,8 @@ export interface IViewModel extends ICursorSimpleModel { setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void; tokenizeViewport(): void; setHasFocus(hasFocus: boolean): void; + onCompositionStart(): void; + onCompositionEnd(): void; onDidColorThemeChange(): void; getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[]; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index e73d29f259..7d89eb84b7 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -183,6 +183,14 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.emitOutgoingEvent(new FocusChangedEvent(!hasFocus, hasFocus)); } + public onCompositionStart(): void { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionStartEvent()); + } + + public onCompositionEnd(): void { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionEndEvent()); + } + public onDidColorThemeChange(): void { this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewThemeChangedEvent()); } @@ -924,8 +932,8 @@ export class ViewModel extends Disposable implements IViewModel { public getPosition(): Position { return this._cursor.getPrimaryCursorState().modelState.position; } - public setSelections(source: string | null | undefined, selections: readonly ISelection[]): void { - this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections)); + public setSelections(source: string | null | undefined, selections: readonly ISelection[], reason = CursorChangeReason.NotSet): void { + this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections, reason)); } public saveCursorState(): ICursorState[] { return this._cursor.saveState(); diff --git a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts index 153793cc21..a7d2a81baf 100644 --- a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts @@ -39,20 +39,20 @@ suite('bracket matching', () => { // start on closing bracket editor.setPosition(new Position(1, 20)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); // start on opening bracket editor.setPosition(new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 31)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 31)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 31)); bracketMatchingController.dispose(); }); @@ -71,25 +71,25 @@ suite('bracket matching', () => { // start position between brackets editor.setPosition(new Position(1, 16)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 18)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 14)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 14)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 18)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 18)); // skip brackets in comments editor.setPosition(new Position(1, 21)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 24)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 24)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 23)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 23)); // do not break if no brackets are available editor.setPosition(new Position(1, 26)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); bracketMatchingController.dispose(); }); @@ -109,32 +109,32 @@ suite('bracket matching', () => { // start position in open brackets editor.setPosition(new Position(1, 9)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 20)); - assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); // start position in close brackets editor.setPosition(new Position(1, 20)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 20)); - assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 20)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 9, 1, 20)); // start position between brackets editor.setPosition(new Position(1, 16)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 19)); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 19)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 19)); // start position outside brackets editor.setPosition(new Position(1, 21)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 25)); - assert.deepEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 25)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 23, 1, 25)); // do not break if no brackets are available editor.setPosition(new Position(1, 26)); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getPosition(), new Position(1, 26)); - assert.deepEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 26)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 26, 1, 26)); bracketMatchingController.dispose(); }); @@ -159,7 +159,7 @@ suite('bracket matching', () => { editor.setPosition(new Position(3, 5)); bracketMatchingController.jumpToBracket(); - assert.deepEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); bracketMatchingController.dispose(); }); @@ -184,7 +184,7 @@ suite('bracket matching', () => { editor.setPosition(new Position(3, 5)); bracketMatchingController.selectToBracket(false); - assert.deepEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 12, 5, 1)); bracketMatchingController.dispose(); }); @@ -207,7 +207,7 @@ suite('bracket matching', () => { new Selection(1, 17, 1, 17) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) @@ -220,7 +220,7 @@ suite('bracket matching', () => { new Selection(1, 14, 1, 14) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) @@ -233,7 +233,7 @@ suite('bracket matching', () => { new Selection(1, 19, 1, 19) ]); bracketMatchingController.selectToBracket(true); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(1, 8, 1, 13), new Selection(1, 16, 1, 19) diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index c81befdd08..69b71ebd47 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -24,10 +24,11 @@ const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; const supportsCut = (platform.isNative || document.queryCommandSupported('cut')); const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard -const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); +const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeLegacy); // Firefox only supports navigator.clipboard.readText() in browser extensions. // See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility -const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true); +// When loading over http, navigator.clipboard can be undefined. See https://github.com/microsoft/monaco-editor/issues/2313 +const supportsPaste = (typeof navigator.clipboard === 'undefined' || browser.isFirefox) ? document.queryCommandSupported('paste') : true; function registerCommand(command: T): T { command.register(); diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 939527a5a5..194f0e89d0 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -106,7 +106,7 @@ export class CodeLensContribution implements IEditorContribution { .monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; } `; if (fontFamily) { - newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: ${fontFamily}}`; + newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: var(${fontFamilyVar})}`; } this._styleElement.textContent = newStyle; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit'); diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index f689a46037..b256664ea8 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -14,7 +14,7 @@ import { editorCodeLensForeground } from 'vs/editor/common/view/editorColorRegis import { CodeLensItem } from 'vs/editor/contrib/codelens/codelens'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; class CodeLensViewZone implements IViewZone { @@ -88,7 +88,7 @@ class CodeLensContentWidget implements IContentWidget { } hasSymbol = true; if (lens.command) { - const title = renderCodicons(lens.command.title.trim()); + const title = renderLabelWithIcons(lens.command.title.trim()); if (lens.command.id) { children.push(dom.$('a', { id: String(i) }, ...title)); this._commands.set(String(i), lens.command); diff --git a/src/vs/editor/contrib/colorPicker/colorContributions.ts b/src/vs/editor/contrib/colorPicker/colorContributions.ts index 7765ec2faa..82053c2110 100644 --- a/src/vs/editor/contrib/colorPicker/colorContributions.ts +++ b/src/vs/editor/contrib/colorPicker/colorContributions.ts @@ -47,7 +47,7 @@ export class ColorContribution extends Disposable implements IEditorContribution } const hoverController = this._editor.getContribution(ModesHoverController.ID); - if (!hoverController.contentWidget.isColorPickerVisible()) { + if (!hoverController.isColorPickerVisible()) { const range = new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1); hoverController.showContentHover(range, HoverStartMode.Delayed, false); } diff --git a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts index 746af27814..0198e5f02b 100644 --- a/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/lineCommentCommand.test.ts @@ -96,25 +96,25 @@ suite('Editor Contrib - Line Comment Command', () => { throw new Error(`unexpected`); } - assert.equal(r.shouldRemoveComments, false); + assert.strictEqual(r.shouldRemoveComments, false); // Does not change `commentStr` - assert.equal(r.lines[0].commentStr, '//'); - assert.equal(r.lines[1].commentStr, 'rem'); - assert.equal(r.lines[2].commentStr, '!@#'); - assert.equal(r.lines[3].commentStr, '!@#'); + assert.strictEqual(r.lines[0].commentStr, '//'); + assert.strictEqual(r.lines[1].commentStr, 'rem'); + assert.strictEqual(r.lines[2].commentStr, '!@#'); + assert.strictEqual(r.lines[3].commentStr, '!@#'); // Fills in `isWhitespace` - assert.equal(r.lines[0].ignore, true); - assert.equal(r.lines[1].ignore, true); - assert.equal(r.lines[2].ignore, false); - assert.equal(r.lines[3].ignore, false); + assert.strictEqual(r.lines[0].ignore, true); + assert.strictEqual(r.lines[1].ignore, true); + assert.strictEqual(r.lines[2].ignore, false); + assert.strictEqual(r.lines[3].ignore, false); // Fills in `commentStrOffset` - assert.equal(r.lines[0].commentStrOffset, 2); - assert.equal(r.lines[1].commentStrOffset, 4); - assert.equal(r.lines[2].commentStrOffset, 4); - assert.equal(r.lines[3].commentStrOffset, 2); + assert.strictEqual(r.lines[0].commentStrOffset, 2); + assert.strictEqual(r.lines[1].commentStrOffset, 4); + assert.strictEqual(r.lines[2].commentStrOffset, 4); + assert.strictEqual(r.lines[3].commentStrOffset, 2); r = LineCommentCommand._analyzeLines(Type.Toggle, true, createSimpleModel([ @@ -127,31 +127,31 @@ suite('Editor Contrib - Line Comment Command', () => { throw new Error(`unexpected`); } - assert.equal(r.shouldRemoveComments, true); + assert.strictEqual(r.shouldRemoveComments, true); // Does not change `commentStr` - assert.equal(r.lines[0].commentStr, '//'); - assert.equal(r.lines[1].commentStr, 'rem'); - assert.equal(r.lines[2].commentStr, '!@#'); - assert.equal(r.lines[3].commentStr, '!@#'); + assert.strictEqual(r.lines[0].commentStr, '//'); + assert.strictEqual(r.lines[1].commentStr, 'rem'); + assert.strictEqual(r.lines[2].commentStr, '!@#'); + assert.strictEqual(r.lines[3].commentStr, '!@#'); // Fills in `isWhitespace` - assert.equal(r.lines[0].ignore, true); - assert.equal(r.lines[1].ignore, false); - assert.equal(r.lines[2].ignore, false); - assert.equal(r.lines[3].ignore, false); + assert.strictEqual(r.lines[0].ignore, true); + assert.strictEqual(r.lines[1].ignore, false); + assert.strictEqual(r.lines[2].ignore, false); + assert.strictEqual(r.lines[3].ignore, false); // Fills in `commentStrOffset` - assert.equal(r.lines[0].commentStrOffset, 2); - assert.equal(r.lines[1].commentStrOffset, 4); - assert.equal(r.lines[2].commentStrOffset, 4); - assert.equal(r.lines[3].commentStrOffset, 2); + assert.strictEqual(r.lines[0].commentStrOffset, 2); + assert.strictEqual(r.lines[1].commentStrOffset, 4); + assert.strictEqual(r.lines[2].commentStrOffset, 4); + assert.strictEqual(r.lines[3].commentStrOffset, 2); // Fills in `commentStrLength` - assert.equal(r.lines[0].commentStrLength, 2); - assert.equal(r.lines[1].commentStrLength, 4); - assert.equal(r.lines[2].commentStrLength, 4); - assert.equal(r.lines[3].commentStrLength, 3); + assert.strictEqual(r.lines[0].commentStrLength, 2); + assert.strictEqual(r.lines[1].commentStrLength, 4); + assert.strictEqual(r.lines[2].commentStrLength, 4); + assert.strictEqual(r.lines[3].commentStrLength, 3); }); test('_normalizeInsertionPoint', () => { @@ -166,7 +166,7 @@ suite('Editor Contrib - Line Comment Command', () => { }); LineCommentCommand._normalizeInsertionPoint(model, offsets, 1, tabSize); const actual = offsets.map(item => item.commentStrOffset); - assert.deepEqual(actual, expected, testName); + assert.deepStrictEqual(actual, expected, testName); }; // Bug 16696:[comment] comments not aligned in this case @@ -1083,7 +1083,7 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { tokenize: () => { throw new Error('not implemented'); }, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { let languageId = (/^ /.test(line) ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID); let tokens = new Uint32Array(1 << 1); diff --git a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts index 3f9f85f554..10f4f875fd 100644 --- a/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts +++ b/src/vs/editor/contrib/cursorUndo/test/cursorUndo.test.ts @@ -29,16 +29,16 @@ suite('FindController', () => { // press Delete CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, {}); - assert.deepEqual(editor.getValue(), 'hell'); - assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); + assert.deepStrictEqual(editor.getValue(), 'hell'); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); // press left CoreNavigationCommands.CursorLeft.runEditorCommand(null, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]); // press Ctrl+U cursorUndoAction.run(null!, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]); }); }); @@ -52,12 +52,12 @@ suite('FindController', () => { // type hello editor.trigger('test', Handler.Type, { text: 'hell' }); editor.trigger('test', Handler.Type, { text: 'o' }); - assert.deepEqual(editor.getValue(), 'hello'); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(editor.getValue(), 'hello'); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); // press Ctrl+U cursorUndoAction.run(null!, editor, {}); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); }); }); diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index 71a503bbbf..54c5af650e 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -20,6 +20,7 @@ import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; function hasTriggerModifier(e: IKeyboardEvent | IMouseEvent): boolean { if (isMacintosh) { @@ -176,8 +177,8 @@ export class DragAndDropController extends Disposable implements IEditorContribu } }); } - // Use `mouse` as the source instead of `api`. - (this._editor).setSelections(newSelections || [], 'mouse'); + // Use `mouse` as the source instead of `api` and setting the reason to explicit (to behave like any other mouse operation). + (this._editor).setSelections(newSelections || [], 'mouse', CursorChangeReason.Explicit); } else if (!this._dragSelection.containsPosition(newCursorPosition) || ( ( diff --git a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts similarity index 54% rename from src/vs/editor/contrib/gotoSymbol/documentSymbols.ts rename to src/vs/editor/contrib/documentSymbols/documentSymbols.ts index 0d670bc72c..23e431cddd 100644 --- a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts +++ b/src/vs/editor/contrib/documentSymbols/documentSymbols.ts @@ -4,62 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; -import { Iterable } from 'vs/base/common/iterator'; export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise { - const model = await OutlineModel.create(document, token); - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (token.isCancellationRequested) { - return flatEntries; - } - if (flat) { - flatten(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort(compareEntriesUsingStart); -} - -function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number { - return Range.compareRangesUsingStarts(a.range, b.range); -} - -function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (let entry of entries) { - bucket.push({ - kind: entry.kind, - tags: entry.tags, - name: entry.name, - detail: entry.detail, - containerName: entry.containerName || overrideContainerLabel, - range: entry.range, - selectionRange: entry.selectionRange, - children: undefined, // we flatten it... - }); - if (entry.children) { - flatten(bucket, entry.children, entry.name); - } - } + return flat + ? model.asListOfDocumentSymbols() + : model.getTopLevelSymbols(); } CommandsRegistry.registerCommand('_executeDocumentSymbolProvider', async function (accessor, ...args) { diff --git a/src/vs/editor/contrib/documentSymbols/outline.ts b/src/vs/editor/contrib/documentSymbols/outline.ts deleted file mode 100644 index 035c0ee810..0000000000 --- a/src/vs/editor/contrib/documentSymbols/outline.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export const OutlineViewId = 'outline'; - -export const OutlineViewFiltered = new RawContextKey('outlineFiltered', false); -export const OutlineViewFocused = new RawContextKey('outlineFocused', false); - -export const enum OutlineConfigKeys { - 'icons' = 'outline.icons', - 'problemsEnabled' = 'outline.problems.enabled', - 'problemsColors' = 'outline.problems.colors', - 'problemsBadges' = 'outline.problems.badges' -} diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index 56c6085324..53743dc554 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -14,8 +14,8 @@ import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { Iterable } from 'vs/base/common/iterator'; -import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { URI } from 'vs/base/common/uri'; export abstract class TreeElement { @@ -204,8 +204,6 @@ export class OutlineGroup extends TreeElement { } } - - export class OutlineModel extends TreeElement { private static readonly _requestDurations = new LanguageFeatureRequestDelays(DocumentSymbolProviderRegistry, 350); @@ -445,4 +443,43 @@ export class OutlineModel extends TreeElement { group.updateMarker(marker.slice(0)); } } + + getTopLevelSymbols(): DocumentSymbol[] { + const roots: DocumentSymbol[] = []; + for (const child of this.children.values()) { + if (child instanceof OutlineElement) { + roots.push(child.symbol); + } else { + roots.push(...Iterable.map(child.children.values(), child => child.symbol)); + } + } + return roots.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + } + + asListOfDocumentSymbols(): DocumentSymbol[] { + const roots = this.getTopLevelSymbols(); + const bucket: DocumentSymbol[] = []; + OutlineModel._flattenDocumentSymbols(bucket, roots, ''); + return bucket.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + } + + private static _flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { + for (const entry of entries) { + bucket.push({ + kind: entry.kind, + tags: entry.tags, + name: entry.name, + detail: entry.detail, + containerName: entry.containerName || overrideContainerLabel, + range: entry.range, + selectionRange: entry.selectionRange, + children: undefined, // we flatten it... + }); + + // Recurse over children + if (entry.children) { + OutlineModel._flattenDocumentSymbols(bucket, entry.children, entry.name); + } + } + } } diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index df4c4d985c..1020dfa768 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -60,14 +60,14 @@ } -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .mirror { padding-right: 22px; } -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .mirror, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { +.monaco-editor .find-widget > .find-part .monaco-inputbox > .ibwrapper > .input, +.monaco-editor .find-widget > .find-part .monaco-inputbox > .ibwrapper > .mirror, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .input, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .ibwrapper > .mirror { padding-top: 2px; padding-bottom: 2px; } diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index e8c0305c05..7f020a67a1 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -380,7 +380,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } private _delayedUpdateHistory() { - this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)); + this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)).then(undefined, onUnexpectedError); } private _updateHistory() { @@ -1044,7 +1044,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL // Toggle selection button this._toggleSelectionFind = this._register(new Checkbox({ - icon: ThemeIcon.asCSSIcon(findSelectionIcon), + icon: findSelectionIcon, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), isChecked: false })); diff --git a/src/vs/editor/contrib/find/test/find.test.ts b/src/vs/editor/contrib/find/test/find.test.ts index 3cf1791d29..c966b67bf8 100644 --- a/src/vs/editor/contrib/find/test/find.test.ts +++ b/src/vs/editor/contrib/find/test/find.test.ts @@ -20,17 +20,17 @@ suite('Find', () => { // The cursor is at the very top, of the file, at the first ABC let searchStringAtTop = getSelectionSearchString(editor); - assert.equal(searchStringAtTop, 'ABC'); + assert.strictEqual(searchStringAtTop, 'ABC'); // Move cursor to the end of ABC editor.setPosition(new Position(1, 3)); let searchStringAfterABC = getSelectionSearchString(editor); - assert.equal(searchStringAfterABC, 'ABC'); + assert.strictEqual(searchStringAfterABC, 'ABC'); // Move cursor to DEF editor.setPosition(new Position(1, 5)); let searchStringInsideDEF = getSelectionSearchString(editor); - assert.equal(searchStringInsideDEF, 'DEF'); + assert.strictEqual(searchStringInsideDEF, 'DEF'); }); }); @@ -44,17 +44,17 @@ suite('Find', () => { // Select A of ABC editor.setSelection(new Range(1, 1, 1, 2)); let searchStringSelectionA = getSelectionSearchString(editor); - assert.equal(searchStringSelectionA, 'A'); + assert.strictEqual(searchStringSelectionA, 'A'); // Select BC of ABC editor.setSelection(new Range(1, 2, 1, 4)); let searchStringSelectionBC = getSelectionSearchString(editor); - assert.equal(searchStringSelectionBC, 'BC'); + assert.strictEqual(searchStringSelectionBC, 'BC'); // Select BC DE editor.setSelection(new Range(1, 2, 1, 7)); let searchStringSelectionBCDE = getSelectionSearchString(editor); - assert.equal(searchStringSelectionBCDE, 'BC DE'); + assert.strictEqual(searchStringSelectionBCDE, 'BC DE'); }); }); @@ -68,17 +68,17 @@ suite('Find', () => { // Select first line and newline editor.setSelection(new Range(1, 1, 2, 1)); let searchStringSelectionWholeLine = getSelectionSearchString(editor); - assert.equal(searchStringSelectionWholeLine, null); + assert.strictEqual(searchStringSelectionWholeLine, null); // Select first line and chunk of second editor.setSelection(new Range(1, 1, 2, 4)); let searchStringSelectionTwoLines = getSelectionSearchString(editor); - assert.equal(searchStringSelectionTwoLines, null); + assert.strictEqual(searchStringSelectionTwoLines, null); // Select end of first line newline and chunk of second editor.setSelection(new Range(1, 7, 2, 4)); let searchStringSelectionSpanLines = getSelectionSearchString(editor); - assert.equal(searchStringSelectionSpanLines, null); + assert.strictEqual(searchStringSelectionSpanLines, null); }); }); diff --git a/src/vs/editor/contrib/find/test/findController.test.ts b/src/vs/editor/contrib/find/test/findController.test.ts index 3ee8717347..ab02529db6 100644 --- a/src/vs/editor/contrib/find/test/findController.test.ts +++ b/src/vs/editor/contrib/find/test/findController.test.ts @@ -102,7 +102,7 @@ suite.skip('FindController', async () => { // I hit Ctrl+F to show the Find dialog startFindAction.run(null, editor); - assert.deepEqual(findController.getGlobalBufferTerm(), findController.getState().searchString); + assert.deepStrictEqual(findController.getGlobalBufferTerm(), findController.getState().searchString); findController.dispose(); }); }); @@ -126,9 +126,9 @@ suite.skip('FindController', async () => { let nextMatchFindAction = new NextMatchFindAction(); nextMatchFindAction.run(null, editor); - assert.equal(findState.searchString, 'ABC'); + assert.strictEqual(findState.searchString, 'ABC'); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); findController.dispose(); }); @@ -152,7 +152,7 @@ suite.skip('FindController', async () => { findState.change({ searchString: 'ABC' }, true); - assert.deepEqual(findController.getGlobalBufferTerm(), 'ABC'); + assert.deepStrictEqual(findController.getGlobalBufferTerm(), 'ABC'); findController.dispose(); }); @@ -181,14 +181,14 @@ suite.skip('FindController', async () => { findState.change({ searchString: 'ABC' }, true); // The first ABC is highlighted. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); // I hit Esc to exit the Find dialog. findController.closeFindWidget(); findController.hasFocus = false; // The cursor is now at end of the first line, with ABC on that line highlighted. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]); // I hit delete to remove it and change the text to XYZ. editor.pushUndoStop(); @@ -201,16 +201,16 @@ suite.skip('FindController', async () => { // ABC // XYZ // ABC - assert.equal(editor.getModel()!.getLineContent(1), 'XYZ'); + assert.strictEqual(editor.getModel()!.getLineContent(1), 'XYZ'); // The cursor is at end of the first line. - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 4, 1, 4]); // I hit F3 to "Find Next" to find the next occurrence of ABC, but instead it searches for XYZ. await nextMatchFindAction.run(null, editor); - assert.equal(findState.searchString, 'ABC'); - assert.equal(findController.hasFocus, false); + assert.strictEqual(findState.searchString, 'ABC'); + assert.strictEqual(findController.hasFocus, false); findController.dispose(); }); @@ -230,10 +230,10 @@ suite.skip('FindController', async () => { }); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]); findController.dispose(); }); @@ -256,10 +256,10 @@ suite.skip('FindController', async () => { await startFindAction.run(null, editor); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]); await nextMatchFindAction.run(null, editor); - assert.deepEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]); findController.dispose(); }); @@ -288,7 +288,7 @@ suite.skip('FindController', async () => { await nextMatchFindAction.run(null, editor); await startFindReplaceAction.run(null, editor); - assert.equal(findController.getState().searchString, testRegexString); + assert.strictEqual(findController.getState().searchString, testRegexString); findController.dispose(); }); @@ -312,16 +312,16 @@ suite.skip('FindController', async () => { loop: true }); - assert.equal(findController.getState().searchScope, null); + assert.strictEqual(findController.getState().searchScope, null); findController.getState().change({ searchScope: [new Range(1, 1, 1, 5)] }, false); - assert.deepEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Range(1, 1, 1, 5)]); findController.closeFindWidget(); - assert.equal(findController.getState().searchScope, null); + assert.strictEqual(findController.getState().searchScope, null); }); }); @@ -338,13 +338,13 @@ suite.skip('FindController', async () => { findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false); findController.moveToNextMatch(); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [1, 39, 1, 42] ]); findController.replace(); - assert.deepEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); + assert.deepStrictEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); findController.dispose(); }); @@ -365,13 +365,13 @@ suite.skip('FindController', async () => { findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false); findController.moveToNextMatch(); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [2, 1, 2, 1] ]); findController.replace(); - assert.deepEqual(editor.getValue(), '\nxline2\nline3'); + assert.deepStrictEqual(editor.getValue(), '\nxline2\nline3'); findController.dispose(); }); @@ -396,7 +396,7 @@ suite.skip('FindController', async () => { // cmd+f3 await nextSelectionMatchFindAction.run(null, editor); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [3, 1, 3, 9] ]); @@ -427,7 +427,7 @@ suite.skip('FindController', async () => { // cmd+f3 await nextSelectionMatchFindAction.run(null, editor); - assert.deepEqual(editor.getSelections()!.map(fromSelection), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromSelection), [ [3, 1, 3, 9] ]); @@ -458,7 +458,7 @@ suite.skip('FindController', async () => { await startFindWithSelectionAction.run(null, editor); let findState = findController.getState(); - assert.deepEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']); + assert.deepStrictEqual(findState.searchString.split(/\r\n|\r|\n/g), ['ABC', 'ABC']); editor.setSelection(new Selection(3, 1, 3, 1)); await startFindWithSelectionAction.run(null, editor); @@ -483,7 +483,7 @@ suite.skip('FindController', async () => { startFindWithSelectionAction.run(null, editor); let findState = findController.getState(); - assert.deepEqual(findState.searchString, 'ABC'); + assert.deepStrictEqual(findState.searchString, 'ABC'); findController.dispose(); }); }); @@ -531,7 +531,7 @@ suite.skip('FindController query options persistence', async () => { // I type ABC. findState.change({ searchString: 'ABC' }, true); // The second ABC is highlighted as matchCase is true. - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]); findController.dispose(); }); @@ -558,7 +558,7 @@ suite.skip('FindController query options persistence', async () => { // I type AB. findState.change({ searchString: 'AB' }, true); // The second AB is highlighted as wholeWord is true. - assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]); + assert.deepStrictEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]); findController.dispose(); }); @@ -575,7 +575,7 @@ suite.skip('FindController query options persistence', async () => { // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); findController.toggleRegex(); - assert.equal(queryState['editor.isRegex'], true); + assert.strictEqual(queryState['editor.isRegex'], true); findController.dispose(); }); @@ -601,13 +601,13 @@ suite.skip('FindController query options persistence', async () => { editor.setSelection(new Range(1, 1, 2, 1)); findController.start(findConfig); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]); findController.closeFindWidget(); editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); findController.start(findConfig); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]); }); }); @@ -631,7 +631,7 @@ suite.skip('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, null); + assert.deepStrictEqual(findController.getState().searchScope, null); }); }); @@ -655,7 +655,7 @@ suite.skip('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]); }); }); @@ -680,7 +680,7 @@ suite.skip('FindController query options persistence', async () => { loop: true }); - assert.deepEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); + assert.deepStrictEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]); }); }); }); diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index ca8bd67a62..91cdb30b02 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -81,13 +81,13 @@ suite('FindModel', () => { } function assertFindState(editor: ICodeEditor, cursor: number[], highlighted: number[] | null, findDecorations: number[][]): void { - assert.deepEqual(fromRange(editor.getSelection()!), cursor, 'cursor'); + assert.deepStrictEqual(fromRange(editor.getSelection()!), cursor, 'cursor'); let expectedState = { highlighted: highlighted ? [highlighted] : [], findDecorations: findDecorations }; - assert.deepEqual(_getFindState(editor), expectedState, 'state'); + assert.deepStrictEqual(_getFindState(editor), expectedState, 'state'); } findTest('incremental find from beginning of file', (editor) => { @@ -245,7 +245,7 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); assertFindState( editor, [1, 1, 1, 1], @@ -275,7 +275,7 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); assertFindState( editor, [1, 1, 1, 1], @@ -290,7 +290,7 @@ suite('FindModel', () => { ); findState.change({ searchString: 'helloo' }, false); - assert.equal(findState.matchesCount, 0); + assert.strictEqual(findState.matchesCount, 0); assertFindState( editor, [1, 1, 1, 1], @@ -1306,7 +1306,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1320,7 +1320,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1333,7 +1333,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, hi!" << endl;'); findModel.replace(); assertFindState( @@ -1345,7 +1345,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1356,7 +1356,7 @@ suite('FindModel', () => { [6, 14, 6, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1365,7 +1365,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); findModel.dispose(); findState.dispose(); @@ -1398,7 +1398,7 @@ suite('FindModel', () => { [11, 10, 11, 13] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// blablablaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// blablablaciao'); findModel.replace(); assertFindState( @@ -1410,7 +1410,7 @@ suite('FindModel', () => { [11, 11, 11, 14] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaoblablaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaoblablaciao'); findModel.replace(); assertFindState( @@ -1421,7 +1421,7 @@ suite('FindModel', () => { [11, 12, 11, 15] ] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaoblaciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaoblaciao'); findModel.replace(); assertFindState( @@ -1430,7 +1430,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); findModel.dispose(); findState.dispose(); @@ -1467,7 +1467,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replaceAll(); assertFindState( @@ -1476,9 +1476,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, hi!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1517,10 +1517,10 @@ suite('FindModel', () => { [9, 1, 9, 3] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hello world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "helloworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "helloworld again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1549,7 +1549,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// ciaociaociaociao'); findModel.dispose(); findState.dispose(); @@ -1578,10 +1578,10 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(11), '// <'); - assert.equal(editor.getModel()!.getLineContent(12), '\t><'); - assert.equal(editor.getModel()!.getLineContent(13), '\t><'); - assert.equal(editor.getModel()!.getLineContent(14), '\t>ciao'); + assert.strictEqual(editor.getModel()!.getLineContent(11), '// <'); + assert.strictEqual(editor.getModel()!.getLineContent(12), '\t><'); + assert.strictEqual(editor.getModel()!.getLineContent(13), '\t><'); + assert.strictEqual(editor.getModel()!.getLineContent(14), '\t>ciao'); findModel.dispose(); findState.dispose(); @@ -1610,8 +1610,8 @@ suite('FindModel', () => { [] ); - assert.equal(editor.getModel()!.getLineContent(2), '#bar "cool.h"'); - assert.equal(editor.getModel()!.getLineContent(3), '#bar '); + assert.strictEqual(editor.getModel()!.getLineContent(2), '#bar "cool.h"'); + assert.strictEqual(editor.getModel()!.getLineContent(3), '#bar '); findModel.dispose(); findState.dispose(); @@ -1665,7 +1665,7 @@ suite('FindModel', () => { findModel.selectAllMatches(); - assert.deepEqual(editor!.getSelections()!.map(s => s.toString()), [ + assert.deepStrictEqual(editor!.getSelections()!.map(s => s.toString()), [ new Selection(6, 14, 6, 19), new Selection(6, 27, 6, 32), new Selection(7, 14, 7, 19), @@ -1709,14 +1709,14 @@ suite('FindModel', () => { findModel.selectAllMatches(); - assert.deepEqual(editor!.getSelections()!.map(s => s.toString()), [ + assert.deepStrictEqual(editor!.getSelections()!.map(s => s.toString()), [ new Selection(7, 14, 7, 19), new Selection(6, 14, 6, 19), new Selection(6, 27, 6, 32), new Selection(8, 14, 8, 19) ].map(s => s.toString())); - assert.deepEqual(editor!.getSelection()!.toString(), new Selection(7, 14, 7, 19).toString()); + assert.deepStrictEqual(editor!.getSelection()!.toString(), new Selection(7, 14, 7, 19).toString()); assertFindState( editor, @@ -1800,7 +1800,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1812,7 +1812,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1823,7 +1823,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1832,7 +1832,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1871,7 +1871,7 @@ suite('FindModel', () => { ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello world again" << endl;'); findModel.replace(); assertFindState( @@ -1883,7 +1883,7 @@ suite('FindModel', () => { [7, 14, 7, 19], ] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); findModel.replace(); assertFindState( @@ -1894,7 +1894,7 @@ suite('FindModel', () => { [7, 14, 7, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1903,7 +1903,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -1927,9 +1927,9 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hi world again" << endl;'); assertFindState( editor, @@ -1970,7 +1970,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1982,7 +1982,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hilo world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hilo world, Hello!" << endl;'); findModel.replace(); assertFindState( @@ -1993,7 +1993,7 @@ suite('FindModel', () => { [8, 14, 8, 19] ] ); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hilo world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hilo world again" << endl;'); findModel.replace(); assertFindState( @@ -2002,7 +2002,7 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "hilo world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "hilo world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2027,10 +2027,10 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hello girl again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "hellogirl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "hellogirl again" << endl;'); assertFindState( editor, @@ -2060,8 +2060,8 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hello girl, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Hello girl again" << endl;'); assertFindState( editor, @@ -2094,10 +2094,10 @@ suite('FindModel', () => { findModel.replaceAll(); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "goodbye world, Goodbye!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "goodbye world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << "Goodbye world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "goodbyeworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "goodbye world, Goodbye!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "goodbye world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << "Goodbye world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "goodbyeworld again" << endl;'); assertFindState( editor, @@ -2134,9 +2134,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << " world, !" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << " world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(8), ' cout << " world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << " world, !" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << " world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(8), ' cout << " world again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2159,7 +2159,7 @@ suite('FindModel', () => { expectedText += 'a line' + i + '\n'; } expectedText += 'a '; - assert.equal(editor!.getModel()!.getValue(), expectedText); + assert.strictEqual(editor!.getModel()!.getValue(), expectedText); findModel.dispose(); findState.dispose(); @@ -2188,9 +2188,9 @@ suite('FindModel', () => { null, [] ); - assert.equal(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); - assert.equal(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); - assert.equal(editor.getModel()!.getLineContent(9), ' cout << "hiworld again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(6), ' cout << "hi world, Hello!" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(7), ' cout << "hi world again" << endl;'); + assert.strictEqual(editor.getModel()!.getLineContent(9), ' cout << "hiworld again" << endl;'); findModel.dispose(); findState.dispose(); @@ -2219,78 +2219,78 @@ suite('FindModel', () => { findState.change({ searchString: 'hello', loop: false }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); // Test next operations - assert.equal(findState.matchesPosition, 0); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 0); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), false); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), false); + assert.strictEqual(findState.canNavigateBack(), true); // Test previous operations findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), false); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), false); }); @@ -2299,78 +2299,78 @@ suite('FindModel', () => { findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); - assert.equal(findState.matchesCount, 5); + assert.strictEqual(findState.matchesCount, 5); // Test next operations - assert.equal(findState.matchesPosition, 0); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 0); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToNextMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); // Test previous operations findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 5); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 5); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 4); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 4); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 3); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 3); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 2); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 2); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); findModel.moveToPrevMatch(); - assert.equal(findState.matchesPosition, 1); - assert.equal(findState.canNavigateForward(), true); - assert.equal(findState.canNavigateBack(), true); + assert.strictEqual(findState.matchesPosition, 1); + assert.strictEqual(findState.canNavigateForward(), true); + assert.strictEqual(findState.canNavigateBack(), true); }); diff --git a/src/vs/editor/contrib/find/test/replacePattern.test.ts b/src/vs/editor/contrib/find/test/replacePattern.test.ts index 6443d5f275..61548df2bc 100644 --- a/src/vs/editor/contrib/find/test/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/replacePattern.test.ts @@ -13,7 +13,7 @@ suite('Replace Pattern test', () => { let testParse = (input: string, expectedPieces: ReplacePiece[]) => { let actual = parseReplaceString(input); let expected = new ReplacePattern(expectedPieces); - assert.deepEqual(actual, expected, 'Parsing ' + input); + assert.deepStrictEqual(actual, expected, 'Parsing ' + input); }; // no backslash => no treatment @@ -73,14 +73,14 @@ suite('Replace Pattern test', () => { let testParse = (input: string, expectedPieces: ReplacePiece[]) => { let actual = parseReplaceString(input); let expected = new ReplacePattern(expectedPieces); - assert.deepEqual(actual, expected, 'Parsing ' + input); + assert.deepStrictEqual(actual, expected, 'Parsing ' + input); }; function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } // \U, \u => uppercase \L, \l => lowercase \E => cancel @@ -107,7 +107,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.deepEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); + assert.deepStrictEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); }; testJSReplaceSemantics('hi', /hi/, 'hello', 'hi'.replace(/hi/, 'hello')); @@ -136,7 +136,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } assertReplace('bla', /bla/, 'hello', 'hello'); @@ -162,7 +162,7 @@ suite('Replace Pattern test', () => { let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); - assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + assert.strictEqual(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); } assertReplace('this is a bla text', /bla/, 'hello', 'hello'); assertReplace('this is a bla text', /this(?=.*bla)/, 'that', 'that'); @@ -184,14 +184,14 @@ suite('Replace Pattern test', () => { let replacePattern = parseReplaceString('a{$1}'); let matches = /a(z)?/.exec('abcd'); let actual = replacePattern.buildReplaceString(matches); - assert.equal(actual, 'a{}'); + assert.strictEqual(actual, 'a{}'); }); test('buildReplaceStringWithCasePreserved test', () => { function assertReplace(target: string[], replaceString: string, expected: string): void { let actual: string = ''; actual = buildReplaceStringWithCasePreserved(target, replaceString); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } assertReplace(['abc'], 'Def', 'def'); @@ -219,7 +219,7 @@ suite('Replace Pattern test', () => { function assertReplace(target: string[], replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let actual = replacePattern.buildReplaceString(target, true); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } assertReplace(['abc'], 'Def', 'def'); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 5ea26960ef..cae8bdc0b1 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -16,13 +16,12 @@ import { Color } from 'vs/base/common/color'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { getBaseLabel, getPathLabel } from 'vs/base/common/labels'; +import { getBaseLabel } from 'vs/base/common/labels'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; import { PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView'; import { basename } from 'vs/base/common/resources'; import { IAction } from 'vs/base/common/actions'; -import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -31,6 +30,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { splitLines } from 'vs/base/common/strings'; +import { ILabelService } from 'vs/platform/label/common/label'; class MessageWidget { @@ -51,6 +51,7 @@ class MessageWidget { editor: ICodeEditor, onRelatedInformation: (related: IRelatedInformation) => void, private readonly _openerService: IOpenerService, + private readonly _labelService: ILabelService ) { this._editor = editor; @@ -169,7 +170,7 @@ class MessageWidget { let relatedResource = document.createElement('a'); relatedResource.classList.add('filename'); relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; - relatedResource.title = getPathLabel(related.resource, undefined); + relatedResource.title = this._labelService.getUriLabel(related.resource); this._relatedDiagnostics.set(relatedResource, related); let relatedMessage = document.createElement('span'); @@ -248,7 +249,8 @@ export class MarkerNavigationWidget extends PeekViewWidget { @IOpenerService private readonly _openerService: IOpenerService, @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILabelService private readonly _labelService: ILabelService ) { super(editor, { showArrow: true, showFrame: true, isAccessible: true }, instantiationService); this._severity = MarkerSeverity.Warning; @@ -310,13 +312,6 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._icon = dom.append(container, dom.$('')); } - protected _getActionBarOptions(): IActionBarOptions { - return { - ...super._getActionBarOptions(), - orientation: ActionsOrientation.HORIZONTAL - }; - } - protected _fillBody(container: HTMLElement): void { this._parentContainer = container; container.classList.add('marker-widget'); @@ -326,7 +321,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._container = document.createElement('div'); container.appendChild(this._container); - this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService); + this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService, this._labelService); this._disposables.add(this._message); } diff --git a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts index f373cd3e3b..26f9bbfaab 100644 --- a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts +++ b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts @@ -51,7 +51,7 @@ export class OneReference { ); } else { return localize( - 'aria.oneReference.preview', "symbol in {0} on line {1} at column {2}, {3}", + { key: 'aria.oneReference.preview', comment: ['Placeholders are: 0: filename, 1:line number, 2: column number, 3: preview snippet of source code'] }, "symbol in {0} on line {1} at column {2}, {3}", basename(this.uri), this.range.startLineNumber, this.range.startColumn, preview.value ); } diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index c24641a2a1..0ed84a63aa 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -22,11 +22,10 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorHoverBackground, editorHoverBorder, editorHoverHighlight, textCodeBlockBackground, textLinkForeground, editorHoverStatusBarBackground, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ModesHoverController implements IEditorContribution { @@ -35,22 +34,8 @@ export class ModesHoverController implements IEditorContribution { private readonly _toUnhook = new DisposableStore(); private readonly _didChangeConfigurationHandler: IDisposable; - private readonly _contentWidget = new MutableDisposable(); - private readonly _glyphWidget = new MutableDisposable(); - - get contentWidget(): ModesContentHoverWidget { - if (!this._contentWidget.value) { - this._createHoverWidgets(); - } - return this._contentWidget.value!; - } - - get glyphWidget(): ModesGlyphHoverWidget { - if (!this._glyphWidget.value) { - this._createHoverWidgets(); - } - return this._glyphWidget.value!; - } + private _contentWidget: ModesContentHoverWidget | null; + private _glyphWidget: ModesGlyphHoverWidget | null; private _isMouseDown: boolean; private _hoverClicked: boolean; @@ -64,15 +49,16 @@ export class ModesHoverController implements IEditorContribution { } constructor(private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IModeService private readonly _modeService: IModeService, - @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, @IContextKeyService _contextKeyService: IContextKeyService ) { this._isMouseDown = false; this._hoverClicked = false; + this._contentWidget = null; + this._glyphWidget = null; this._hookEvents(); @@ -113,8 +99,8 @@ export class ModesHoverController implements IEditorContribution { } private _onModelDecorationsChanged(): void { - this.contentWidget.onModelDecorationsChanged(); - this.glyphWidget.onModelDecorationsChanged(); + this._contentWidget?.onModelDecorationsChanged(); + this._glyphWidget?.onModelDecorationsChanged(); } private _onEditorScrollChanged(e: IScrollEvent): void { @@ -153,7 +139,7 @@ export class ModesHoverController implements IEditorContribution { private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void { let targetType = mouseEvent.target.type; - if (this._isMouseDown && this._hoverClicked && this.contentWidget.isColorPickerVisible()) { + if (this._isMouseDown && this._hoverClicked) { return; } @@ -162,10 +148,14 @@ export class ModesHoverController implements IEditorContribution { return; } + if (this._isHoverSticky && !mouseEvent.event.browserEvent.view?.getSelection()?.isCollapsed) { + // selected text within content hover widget + return; + } if ( !this._isHoverSticky && targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID - && this._contentWidget.value?.isColorPickerVisible() + && this._contentWidget?.isColorPickerVisible() ) { // though the hover is not sticky, the color picker needs to. return; @@ -186,26 +176,31 @@ export class ModesHoverController implements IEditorContribution { } if (targetType === MouseTargetType.CONTENT_TEXT) { - this.glyphWidget.hide(); + this._glyphWidget?.hide(); if (this._isHoverEnabled && mouseEvent.target.range) { // TODO@rebornix. This should be removed if we move Color Picker out of Hover component. // Check if mouse is hovering on color decorator const hoverOnColorDecorator = [...mouseEvent.target.element?.classList.values() || []].find(className => className.startsWith('ced-colorBox')) && mouseEvent.target.range.endColumn - mouseEvent.target.range.startColumn === 1; - if (hoverOnColorDecorator) { - // shift the mouse focus by one as color decorator is a `before` decoration of next character. - this.contentWidget.startShowingAt(new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1), HoverStartMode.Delayed, false); - } else { - this.contentWidget.startShowingAt(mouseEvent.target.range, HoverStartMode.Delayed, false); + const showAtRange = ( + hoverOnColorDecorator // shift the mouse focus by one as color decorator is a `before` decoration of next character. + ? new Range(mouseEvent.target.range.startLineNumber, mouseEvent.target.range.startColumn + 1, mouseEvent.target.range.endLineNumber, mouseEvent.target.range.endColumn + 1) + : mouseEvent.target.range + ); + if (!this._contentWidget) { + this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); } - + this._contentWidget.startShowingAt(showAtRange, HoverStartMode.Delayed, false); } } else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) { - this.contentWidget.hide(); + this._contentWidget?.hide(); if (this._isHoverEnabled && mouseEvent.target.position) { - this.glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); + if (!this._glyphWidget) { + this._glyphWidget = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + } + this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber); } } else { this._hideWidgets(); @@ -220,29 +215,32 @@ export class ModesHoverController implements IEditorContribution { } private _hideWidgets(): void { - if (!this._glyphWidget.value || !this._contentWidget.value || (this._isMouseDown && this._hoverClicked && this._contentWidget.value.isColorPickerVisible())) { + if ((this._isMouseDown && this._hoverClicked && this._contentWidget?.isColorPickerVisible())) { return; } - this._glyphWidget.value.hide(); - this._contentWidget.value.hide(); + this._hoverClicked = false; + this._glyphWidget?.hide(); + this._contentWidget?.hide(); } - private _createHoverWidgets() { - this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._markerDecorationsService, this._keybindingService, this._themeService, this._modeService, this._openerService); - this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); + public isColorPickerVisible(): boolean { + return this._contentWidget?.isColorPickerVisible() || false; } public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void { - this.contentWidget.startShowingAt(range, mode, focus); + if (!this._contentWidget) { + this._contentWidget = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._instantiationService, this._themeService); + } + this._contentWidget.startShowingAt(range, mode, focus); } public dispose(): void { this._unhookEvents(); this._toUnhook.dispose(); this._didChangeConfigurationHandler.dispose(); - this._glyphWidget.dispose(); - this._contentWidget.dispose(); + this._glyphWidget?.dispose(); + this._contentWidget?.dispose(); } } diff --git a/src/vs/editor/contrib/hover/hoverWidgets.ts b/src/vs/editor/contrib/hover/hoverWidgets.ts index 993b9f7cd3..2a6f20430d 100644 --- a/src/vs/editor/contrib/hover/hoverWidgets.ts +++ b/src/vs/editor/contrib/hover/hoverWidgets.ts @@ -3,168 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Widget } from 'vs/base/browser/ui/widget'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { renderHoverAction, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export class ContentHoverWidget extends Widget implements IContentWidget { - - protected readonly _hover: HoverWidget; - private readonly _id: string; - protected _editor: ICodeEditor; - private _isVisible: boolean; - protected _showAtPosition: Position | null; - protected _showAtRange: Range | null; - private _stoleFocus: boolean; - - // Editor.IContentWidget.allowEditorOverflow - public allowEditorOverflow = true; - - protected get isVisible(): boolean { - return this._isVisible; - } - - protected set isVisible(value: boolean) { - this._isVisible = value; - this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); - } - - constructor( - id: string, - editor: ICodeEditor, - private readonly _hoverVisibleKey: IContextKey, - private readonly _keybindingService: IKeybindingService - ) { - super(); - - this._hover = this._register(new HoverWidget()); - this._id = id; - this._editor = editor; - this._isVisible = false; - this._stoleFocus = false; - - this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { - if (e.equals(KeyCode.Escape)) { - this.hide(); - } - }); - - this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { - if (e.hasChanged(EditorOption.fontInfo)) { - this.updateFont(); - } - })); - - this._editor.onDidLayoutChange(e => this.layout()); - - this.layout(); - this._editor.addContentWidget(this); - this._showAtPosition = null; - this._showAtRange = null; - this._stoleFocus = false; - } - - public getId(): string { - return this._id; - } - - public getDomNode(): HTMLElement { - return this._hover.containerDomNode; - } - - public showAt(position: Position, range: Range | null, focus: boolean): void { - // Position has changed - this._showAtPosition = position; - this._showAtRange = range; - this._hoverVisibleKey.set(true); - this.isVisible = true; - - this._editor.layoutContentWidget(this); - // Simply force a synchronous render on the editor - // such that the widget does not really render with left = '0px' - this._editor.render(); - this._stoleFocus = focus; - if (focus) { - this._hover.containerDomNode.focus(); - } - } - - public hide(): void { - if (!this.isVisible) { - return; - } - - setTimeout(() => { - // Give commands a chance to see the key - if (!this.isVisible) { - this._hoverVisibleKey.set(false); - } - }, 0); - this.isVisible = false; - - this._editor.layoutContentWidget(this); - if (this._stoleFocus) { - this._editor.focus(); - } - } - - public getPosition(): IContentWidgetPosition | null { - if (this.isVisible) { - return { - position: this._showAtPosition, - range: this._showAtRange, - preference: [ - ContentWidgetPositionPreference.ABOVE, - ContentWidgetPositionPreference.BELOW - ] - }; - } - return null; - } - - public dispose(): void { - this._editor.removeContentWidget(this); - super.dispose(); - } - - private updateFont(): void { - const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); - codeClasses.forEach(node => this._editor.applyFontInfo(node)); - } - - protected updateContents(node: Node): void { - this._hover.contentsDomNode.textContent = ''; - this._hover.contentsDomNode.appendChild(node); - this.updateFont(); - - this._editor.layoutContentWidget(this); - this._hover.onContentsChanged(); - } - - protected _renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { - const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - return renderHoverAction(parent, actionOptions, keybindingLabel); - } - - private layout(): void { - const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); - const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); - - this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; - this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; - this._hover.contentsDomNode.style.maxHeight = `${height}px`; - this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; - } -} export class GlyphHoverWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/editor/contrib/hover/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts new file mode 100644 index 0000000000..337646f023 --- /dev/null +++ b/src/vs/editor/contrib/hover/markdownHoverParticipant.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { asArray } from 'vs/base/common/arrays'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; +import { HoverProviderRegistry } from 'vs/editor/common/modes'; +import { getHover } from 'vs/editor/contrib/hover/getHover'; +import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +const $ = dom.$; + +export class MarkdownHover implements IHoverPart { + + constructor( + public readonly range: Range, + public readonly contents: IMarkdownString[] + ) { } + + public equals(other: IHoverPart): boolean { + if (other instanceof MarkdownHover) { + return markedStringsEquals(this.contents, other.contents); + } + return false; + } +} + +export class MarkdownHoverParticipant implements IEditorHoverParticipant { + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IModeService private readonly _modeService: IModeService, + @IOpenerService private readonly _openerService: IOpenerService, + ) { } + + public createLoadingMessage(range: Range): MarkdownHover { + return new MarkdownHover(range, [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))]); + } + + public computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): MarkdownHover[] { + if (!this._editor.hasModel()) { + return []; + } + + const model = this._editor.getModel(); + const lineNumber = hoverRange.startLineNumber; + const maxColumn = model.getLineMaxColumn(lineNumber); + const result: MarkdownHover[] = []; + for (const d of lineDecorations) { + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + const hoverMessage = d.options.hoverMessage; + if (!hoverMessage || isEmptyMarkdownString(hoverMessage)) { + continue; + } + + const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); + result.push(new MarkdownHover(range, asArray(hoverMessage))); + } + + return result; + } + + public async computeAsync(range: Range, token: CancellationToken): Promise { + if (!this._editor.hasModel() || !range) { + return Promise.resolve([]); + } + + const model = this._editor.getModel(); + + if (!HoverProviderRegistry.has(model)) { + return Promise.resolve([]); + } + + const hovers = await getHover(model, new Position( + range.startLineNumber, + range.startColumn + ), token); + + const result: MarkdownHover[] = []; + for (const hover of hovers) { + if (isEmptyMarkdownString(hover.contents)) { + continue; + } + const rng = hover.range ? Range.lift(hover.range) : range; + result.push(new MarkdownHover(rng, hover.contents)); + } + return result; + } + + public renderHoverParts(hoverParts: MarkdownHover[], fragment: DocumentFragment): IDisposable { + const disposables = new DisposableStore(); + for (const hoverPart of hoverParts) { + for (const contents of hoverPart.contents) { + if (isEmptyMarkdownString(contents)) { + continue; + } + const markdownHoverElement = $('div.hover-row.markdown-hover'); + const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); + const renderer = disposables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService)); + disposables.add(renderer.onDidRenderAsync(() => { + hoverContentsElement.className = 'hover-contents code-hover-contents'; + this._hover.onContentsChanged(); + })); + const renderedContents = disposables.add(renderer.render(contents)); + hoverContentsElement.appendChild(renderedContents.element); + fragment.appendChild(markdownHoverElement); + } + } + return disposables; + } +} diff --git a/src/vs/editor/contrib/hover/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/markerHoverParticipant.ts new file mode 100644 index 0000000000..55f691aadb --- /dev/null +++ b/src/vs/editor/contrib/hover/markerHoverParticipant.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { CodeActionTriggerType } from 'vs/editor/common/modes'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { basename } from 'vs/base/common/resources'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; +import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; +import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; +import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; +import { IModelDecoration } from 'vs/editor/common/model'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IEditorHover, IEditorHoverParticipant, IHoverPart } from 'vs/editor/contrib/hover/modesContentHover'; + +const $ = dom.$; + +export class MarkerHover implements IHoverPart { + + constructor( + public readonly range: Range, + public readonly marker: IMarker, + ) { } + + public equals(other: IHoverPart): boolean { + if (other instanceof MarkerHover) { + return IMarkerData.makeKey(this.marker) === IMarkerData.makeKey(other.marker); + } + return false; + } +} + +const markerCodeActionTrigger: CodeActionTrigger = { + type: CodeActionTriggerType.Manual, + filter: { include: CodeActionKind.QuickFix } +}; + +export class MarkerHoverParticipant implements IEditorHoverParticipant { + + private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined; + + constructor( + private readonly _editor: ICodeEditor, + private readonly _hover: IEditorHover, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IOpenerService private readonly _openerService: IOpenerService, + ) { } + + public computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): MarkerHover[] { + if (!this._editor.hasModel()) { + return []; + } + + const model = this._editor.getModel(); + const lineNumber = hoverRange.startLineNumber; + const maxColumn = model.getLineMaxColumn(lineNumber); + const result: MarkerHover[] = []; + for (const d of lineDecorations) { + const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1; + const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn; + + const marker = this._markerDecorationsService.getMarker(model.uri, d); + if (!marker) { + continue; + } + + const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); + result.push(new MarkerHover(range, marker)); + } + + return result; + } + + public renderHoverParts(hoverParts: MarkerHover[], fragment: DocumentFragment): IDisposable { + if (!hoverParts.length) { + return Disposable.None; + } + const disposables = new DisposableStore(); + hoverParts.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg, disposables))); + const markerHoverForStatusbar = hoverParts.length === 1 ? hoverParts[0] : hoverParts.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; + fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar, disposables)); + return disposables; + } + + private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + const hoverElement = $('div.hover-row'); + const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); + const { source, message, code, relatedInformation } = markerHover.marker; + + this._editor.applyFontInfo(markerElement); + const messageElement = dom.append(markerElement, $('span')); + messageElement.style.whiteSpace = 'pre-wrap'; + messageElement.innerText = message; + + if (source || code) { + // Code has link + if (code && typeof code !== 'string') { + const sourceAndCodeElement = $('span'); + if (source) { + const sourceElement = dom.append(sourceAndCodeElement, $('span')); + sourceElement.innerText = source; + } + const codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); + codeLink.setAttribute('href', code.target.toString()); + + disposables.add(dom.addDisposableListener(codeLink, 'click', (e) => { + this._openerService.open(code.target); + e.preventDefault(); + e.stopPropagation(); + })); + + const codeElement = dom.append(codeLink, $('span')); + codeElement.innerText = code.value; + + const detailsElement = dom.append(markerElement, sourceAndCodeElement); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + } else { + const detailsElement = dom.append(markerElement, $('span')); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; + } + } + + if (isNonEmptyArray(relatedInformation)) { + for (const { message, resource, startLineNumber, startColumn } of relatedInformation) { + const relatedInfoContainer = dom.append(markerElement, $('div')); + relatedInfoContainer.style.marginTop = '8px'; + const a = dom.append(relatedInfoContainer, $('a')); + a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `; + a.style.cursor = 'pointer'; + disposables.add(dom.addDisposableListener(a, 'click', (e) => { + e.stopPropagation(); + e.preventDefault(); + if (this._openerService) { + this._openerService.open(resource, { + fromUserGesture: true, + editorOptions: { selection: { startLineNumber, startColumn } } + }).catch(onUnexpectedError); + } + })); + const messageElement = dom.append(relatedInfoContainer, $('span')); + messageElement.innerText = message; + this._editor.applyFontInfo(messageElement); + } + } + + return hoverElement; + } + + private renderMarkerStatusbar(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + const hoverElement = $('div.hover-row.status-bar'); + const actionsElement = dom.append(hoverElement, $('div.actions')); + if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { + disposables.add(this.renderAction(actionsElement, { + label: nls.localize('peek problem', "Peek Problem"), + commandId: NextMarkerAction.ID, + run: () => { + this._hover.hide(); + MarkerController.get(this._editor).showAtMarker(markerHover.marker); + this._editor.focus(); + } + })); + } + + if (!this._editor.getOption(EditorOption.readOnly)) { + const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); + if (this.recentMarkerCodeActionsInfo) { + if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) { + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + } + } else { + this.recentMarkerCodeActionsInfo = undefined; + } + } + const updatePlaceholderDisposable = this.recentMarkerCodeActionsInfo && !this.recentMarkerCodeActionsInfo.hasCodeActions ? Disposable.None : disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 200)); + if (!quickfixPlaceholderElement.textContent) { + // Have some content in here to avoid flickering + quickfixPlaceholderElement.textContent = String.fromCharCode(0xA0); //   + } + const codeActionsPromise = this.getCodeActions(markerHover.marker); + disposables.add(toDisposable(() => codeActionsPromise.cancel())); + codeActionsPromise.then(actions => { + updatePlaceholderDisposable.dispose(); + this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 }; + + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + actions.dispose(); + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + return; + } + quickfixPlaceholderElement.style.display = 'none'; + + let showing = false; + disposables.add(toDisposable(() => { + if (!showing) { + actions.dispose(); + } + })); + + disposables.add(this.renderAction(actionsElement, { + label: nls.localize('quick fixes', "Quick Fix..."), + commandId: QuickFixAction.Id, + run: (target) => { + showing = true; + const controller = QuickFixController.get(this._editor); + const elementPosition = dom.getDomNodePagePosition(target); + // Hide the hover pre-emptively, otherwise the editor can close the code actions + // context menu as well when using keyboard navigation + this._hover.hide(); + controller.showCodeActions(markerCodeActionTrigger, actions, { + x: elementPosition.left + 6, + y: elementPosition.top + elementPosition.height + 6 + }); + } + })); + }); + } + + return hoverElement; + } + + private renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + return renderHoverAction(parent, actionOptions, keybindingLabel); + } + + private getCodeActions(marker: IMarker): CancelablePromise { + return createCancelablePromise(cancellationToken => { + return getCodeActions( + this._editor.getModel()!, + new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), + markerCodeActionTrigger, + Progress.None, + cancellationToken); + }); + } +} diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index c7ed60658f..ba6a843e84 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -3,228 +3,242 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; -import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; -import { IDisposable, toDisposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { DocumentColorProvider, Hover as MarkdownHover, HoverProviderRegistry, IColor, TokenizationRegistry, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { DocumentColorProvider, IColor, TokenizationRegistry } from 'vs/editor/common/modes'; import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/colorPickerWidget'; -import { getHover } from 'vs/editor/contrib/hover/getHover'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; -import { ContentHoverWidget } from 'vs/editor/contrib/hover/hoverWidgets'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { coalesce, isNonEmptyArray, asArray } from 'vs/base/common/arrays'; -import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { basename } from 'vs/base/common/resources'; -import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; -import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; -import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; -import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; -import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IIdentifiedSingleEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { coalesce } from 'vs/base/common/arrays'; +import { IIdentifiedSingleEditOperation, IModelDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { Progress } from 'vs/platform/progress/common/progress'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; +import { MarkerHover, MarkerHoverParticipant } from 'vs/editor/contrib/hover/markerHoverParticipant'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MarkdownHover, MarkdownHoverParticipant } from 'vs/editor/contrib/hover/markdownHoverParticipant'; -const $ = dom.$; +export interface IHoverPart { + readonly range: Range; + equals(other: IHoverPart): boolean; +} -class ColorHover { +export interface IEditorHover { + hide(): void; + onContentsChanged(): void; +} + +export interface IEditorHoverParticipant { + computeSync(hoverRange: Range, lineDecorations: IModelDecoration[]): T[]; + computeAsync?(range: Range, token: CancellationToken): Promise; + renderHoverParts(hoverParts: T[], fragment: DocumentFragment): IDisposable; +} + +class ColorHover implements IHoverPart { constructor( - public readonly range: IRange, + public readonly range: Range, public readonly color: IColor, public readonly provider: DocumentColorProvider ) { } + + equals(other: IHoverPart): boolean { + return false; + } } -class MarkerHover { - +class HoverPartInfo { constructor( - public readonly range: IRange, - public readonly marker: IMarker, + public readonly owner: IEditorHoverParticipant | null, + public readonly data: IHoverPart ) { } } -type HoverPart = MarkdownHover | ColorHover | MarkerHover; - -class ModesContentComputer implements IHoverComputer { +class ModesContentComputer implements IHoverComputer { private readonly _editor: ICodeEditor; - private _result: HoverPart[]; - private _range?: Range; + private _result: HoverPartInfo[]; + private _range: Range | null; constructor( editor: ICodeEditor, - private readonly _markerDecorationsService: IMarkerDecorationsService + private readonly _markerHoverParticipant: IEditorHoverParticipant, + private readonly _markdownHoverParticipant: MarkdownHoverParticipant ) { this._editor = editor; this._result = []; + this._range = null; } - setRange(range: Range): void { + public setRange(range: Range): void { this._range = range; this._result = []; } - clearResult(): void { + public clearResult(): void { this._result = []; } - computeAsync(token: CancellationToken): Promise { + public async computeAsync(token: CancellationToken): Promise { if (!this._editor.hasModel() || !this._range) { return Promise.resolve([]); } - const model = this._editor.getModel(); - - if (!HoverProviderRegistry.has(model)) { - return Promise.resolve([]); - } - - return getHover(model, new Position( - this._range.startLineNumber, - this._range.startColumn - ), token); + const markdownHovers = await this._markdownHoverParticipant.computeAsync(this._range, token); + return markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h)); } - computeSync(): HoverPart[] { + public computeSync(): HoverPartInfo[] { if (!this._editor.hasModel() || !this._range) { return []; } const model = this._editor.getModel(); - const lineNumber = this._range.startLineNumber; + const hoverRange = this._range; + const lineNumber = hoverRange.startLineNumber; if (lineNumber > this._editor.getModel().getLineCount()) { // Illegal line number => no results return []; } - const colorDetector = ColorDetector.get(this._editor); const maxColumn = model.getLineMaxColumn(lineNumber); - const lineDecorations = this._editor.getLineDecorations(lineNumber); - let didFindColor = false; - - const hoverRange = this._range; - const result = lineDecorations.map((d): HoverPart | null => { + const lineDecorations = this._editor.getLineDecorations(lineNumber).filter((d) => { 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) { - return null; - } - - const range = new Range(hoverRange.startLineNumber, startColumn, hoverRange.startLineNumber, endColumn); - const marker = this._markerDecorationsService.getMarker(model, d); - if (marker) { - return new MarkerHover(range, marker); - } - - const colorData = colorDetector.getColorData(d.range.getStartPosition()); - - if (!didFindColor && colorData) { - didFindColor = true; - - const { color, range } = colorData.colorInfo; - return new ColorHover(range, color, colorData.provider); - } else { - if (isEmptyMarkdownString(d.options.hoverMessage)) { - return null; - } - - const contents: IMarkdownString[] = d.options.hoverMessage ? asArray(d.options.hoverMessage) : []; - return { contents, range }; + return false; } + return true; }); + let result: HoverPartInfo[] = []; + + const colorDetector = ColorDetector.get(this._editor); + for (const d of lineDecorations) { + const colorData = colorDetector.getColorData(d.range.getStartPosition()); + if (colorData) { + const { color, range } = colorData.colorInfo; + result.push(new HoverPartInfo(null, new ColorHover(Range.lift(range), color, colorData.provider))); + break; + } + } + + const markdownHovers = this._markdownHoverParticipant.computeSync(this._range, lineDecorations); + result = result.concat(markdownHovers.map(h => new HoverPartInfo(this._markdownHoverParticipant, h))); + + const markerHovers = this._markerHoverParticipant.computeSync(this._range, lineDecorations); + result = result.concat(markerHovers.map(h => new HoverPartInfo(this._markerHoverParticipant, h))); + return coalesce(result); } - onResult(result: HoverPart[], isFromSynchronousComputation: boolean): void { + public onResult(result: HoverPartInfo[], isFromSynchronousComputation: boolean): void { // Always put synchronous messages before asynchronous ones if (isFromSynchronousComputation) { - this._result = result.concat(this._result.sort((a, b) => { - if (a instanceof ColorHover) { // sort picker messages at to the top - return -1; - } else if (b instanceof ColorHover) { - return 1; - } - return 0; - })); + this._result = result.concat(this._result); } else { this._result = this._result.concat(result); } } - getResult(): HoverPart[] { + public getResult(): HoverPartInfo[] { return this._result.slice(0); } - getResultWithLoadingMessage(): HoverPart[] { - return this._result.slice(0).concat([this._getLoadingMessage()]); - } + public getResultWithLoadingMessage(): HoverPartInfo[] { + if (this._range) { + const loadingMessage = new HoverPartInfo(this._markdownHoverParticipant, this._markdownHoverParticipant.createLoadingMessage(this._range)); + return this._result.slice(0).concat([loadingMessage]); - private _getLoadingMessage(): HoverPart { - return { - range: this._range, - contents: [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))] - }; + } + return this._result.slice(0); } } -const markerCodeActionTrigger: CodeActionTrigger = { - type: CodeActionTriggerType.Manual, - filter: { include: CodeActionKind.QuickFix } -}; - -export class ModesContentHoverWidget extends ContentHoverWidget { +export class ModesContentHoverWidget extends Widget implements IContentWidget, IEditorHover { static readonly ID = 'editor.contrib.modesContentHoverWidget'; - private _messages: HoverPart[]; + private readonly _markerHoverParticipant: IEditorHoverParticipant; + private readonly _markdownHoverParticipant: MarkdownHoverParticipant; + + private readonly _hover: HoverWidget; + private readonly _id: string; + private readonly _editor: ICodeEditor; + private _isVisible: boolean; + private _showAtPosition: Position | null; + private _showAtRange: Range | null; + private _stoleFocus: boolean; + + // IContentWidget.allowEditorOverflow + public readonly allowEditorOverflow = true; + + private _messages: HoverPartInfo[]; private _lastRange: Range | null; private readonly _computer: ModesContentComputer; - private readonly _hoverOperation: HoverOperation; + private readonly _hoverOperation: HoverOperation; private _highlightDecorations: string[]; private _isChangingDecorations: boolean; private _shouldFocus: boolean; private _colorPicker: ColorPickerWidget | null; - - private _codeLink?: HTMLElement; - - private readonly renderDisposable = this._register(new MutableDisposable()); + private _renderDisposable: IDisposable | null; constructor( editor: ICodeEditor, - _hoverVisibleKey: IContextKey, - markerDecorationsService: IMarkerDecorationsService, - keybindingService: IKeybindingService, + private readonly _hoverVisibleKey: IContextKey, + instantiationService: IInstantiationService, private readonly _themeService: IThemeService, - private readonly _modeService: IModeService, - private readonly _openerService: IOpenerService = NullOpenerService, ) { - super(ModesContentHoverWidget.ID, editor, _hoverVisibleKey, keybindingService); + super(); + + this._markerHoverParticipant = instantiationService.createInstance(MarkerHoverParticipant, editor, this); + this._markdownHoverParticipant = instantiationService.createInstance(MarkdownHoverParticipant, editor, this); + + this._hover = this._register(new HoverWidget()); + this._id = ModesContentHoverWidget.ID; + this._editor = editor; + this._isVisible = false; + this._stoleFocus = false; + this._renderDisposable = null; + + this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Escape)) { + this.hide(); + } + }); + + this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (e.hasChanged(EditorOption.fontInfo)) { + this._updateFont(); + } + })); + + this._editor.onDidLayoutChange(() => this.layout()); + + this.layout(); + this._editor.addContentWidget(this); + this._showAtPosition = null; + this._showAtRange = null; + this._stoleFocus = false; this._messages = []; this._lastRange = null; - this._computer = new ModesContentComputer(this._editor, markerDecorationsService); + this._computer = new ModesContentComputer(this._editor, this._markerHoverParticipant, this._markdownHoverParticipant); this._highlightDecorations = []; this._isChangingDecorations = false; this._shouldFocus = false; @@ -246,15 +260,15 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._register(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => { this.getDomNode().classList.remove('colorpicker-hover'); })); - this._register(editor.onDidChangeConfiguration((e) => { + this._register(editor.onDidChangeConfiguration(() => { this._hoverOperation.setHoverTime(this._editor.getOption(EditorOption.hover).delay); })); - this._register(TokenizationRegistry.onDidChange((e) => { - if (this.isVisible && this._lastRange && this._messages.length > 0) { + this._register(TokenizationRegistry.onDidChange(() => { + if (this._isVisible && this._lastRange && this._messages.length > 0) { this._messages = this._messages.map(msg => { // If a color hover is visible, we need to update the message that // created it so that the color matches the last chosen color - if (msg instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.range) && this._colorPicker?.model.color) { + if (msg.data instanceof ColorHover && !!this._lastRange?.intersectRanges(msg.data.range) && this._colorPicker?.model.color) { const color = this._colorPicker.model.color; const newColor = { red: color.rgba.r / 255, @@ -262,7 +276,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { blue: color.rgba.b / 255, alpha: color.rgba.a }; - return new ColorHover(msg.range, newColor, msg.provider); + return new HoverPartInfo(msg.owner, new ColorHover(msg.data.range, newColor, msg.data.provider)); } else { return msg; } @@ -274,16 +288,81 @@ export class ModesContentHoverWidget extends ContentHoverWidget { })); } - dispose(): void { + public dispose(): void { this._hoverOperation.cancel(); + this._editor.removeContentWidget(this); super.dispose(); } - onModelDecorationsChanged(): void { + public getId(): string { + return this._id; + } + + public getDomNode(): HTMLElement { + return this._hover.containerDomNode; + } + + public showAt(position: Position, range: Range | null, focus: boolean): void { + // Position has changed + this._showAtPosition = position; + this._showAtRange = range; + this._hoverVisibleKey.set(true); + this._isVisible = true; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + + this._editor.layoutContentWidget(this); + // Simply force a synchronous render on the editor + // such that the widget does not really render with left = '0px' + this._editor.render(); + this._stoleFocus = focus; + if (focus) { + this._hover.containerDomNode.focus(); + } + } + + public getPosition(): IContentWidgetPosition | null { + if (this._isVisible) { + return { + position: this._showAtPosition, + range: this._showAtRange, + preference: [ + ContentWidgetPositionPreference.ABOVE, + ContentWidgetPositionPreference.BELOW + ] + }; + } + return null; + } + + private _updateFont(): void { + const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); + codeClasses.forEach(node => this._editor.applyFontInfo(node)); + } + + private _updateContents(node: Node): void { + this._hover.contentsDomNode.textContent = ''; + this._hover.contentsDomNode.appendChild(node); + this._updateFont(); + + this._editor.layoutContentWidget(this); + this._hover.onContentsChanged(); + } + + private layout(): void { + const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); + const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); + + this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; + this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; + this._hover.contentsDomNode.style.maxHeight = `${height}px`; + this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; + } + + public onModelDecorationsChanged(): void { if (this._isChangingDecorations) { return; } - if (this.isVisible) { + if (this._isVisible) { // The decorations have changed and the hover is visible, // we need to recompute the displayed text this._hoverOperation.cancel(); @@ -295,7 +374,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } } - startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { + public startShowingAt(range: Range, mode: HoverStartMode, focus: boolean): void { if (this._lastRange && this._lastRange.equalsRange(range)) { // We have to show the widget at the exact same range as before, so no work is needed return; @@ -303,17 +382,17 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._hoverOperation.cancel(); - if (this.isVisible) { + if (this._isVisible) { // The range might have changed, but the hover is visible // Instead of hiding it completely, filter out messages that are still in the new range and // kick off a new computation if (!this._showAtPosition || this._showAtPosition.lineNumber !== range.startLineNumber) { this.hide(); } else { - let filteredMessages: HoverPart[] = []; + let filteredMessages: HoverPartInfo[] = []; for (let i = 0, len = this._messages.length; i < len; i++) { const msg = this._messages[i]; - const rng = msg.range; + const rng = msg.data.range; if (rng && rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) { filteredMessages.push(msg); } @@ -335,25 +414,45 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._hoverOperation.start(mode); } - hide(): void { + public hide(): void { this._lastRange = null; this._hoverOperation.cancel(); - super.hide(); + + if (this._isVisible) { + setTimeout(() => { + // Give commands a chance to see the key + if (!this._isVisible) { + this._hoverVisibleKey.set(false); + } + }, 0); + this._isVisible = false; + this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible); + + this._editor.layoutContentWidget(this); + if (this._stoleFocus) { + this._editor.focus(); + } + } + this._isChangingDecorations = true; this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []); this._isChangingDecorations = false; - this.renderDisposable.clear(); + if (this._renderDisposable) { + this._renderDisposable.dispose(); + this._renderDisposable = null; + } this._colorPicker = null; } - isColorPickerVisible(): boolean { - if (this._colorPicker) { - return true; - } - return false; + public isColorPickerVisible(): boolean { + return !!this._colorPicker; } - private _withResult(result: HoverPart[], complete: boolean): void { + public onContentsChanged(): void { + this._hover.onContentsChanged(); + } + + private _withResult(result: HoverPartInfo[], complete: boolean): void { this._messages = result; if (this._lastRange && this._messages.length > 0) { @@ -363,20 +462,24 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } } - private _renderMessages(renderRange: Range, messages: HoverPart[]): void { - this.renderDisposable.dispose(); + private _renderMessages(renderRange: Range, messages: HoverPartInfo[]): void { + if (this._renderDisposable) { + this._renderDisposable.dispose(); + this._renderDisposable = null; + } this._colorPicker = null; // update column from which to show let renderColumn = Constants.MAX_SAFE_SMALL_INTEGER; - let highlightRange: Range | null = messages[0].range ? Range.lift(messages[0].range) : null; + let highlightRange: Range | null = messages[0].data.range ? Range.lift(messages[0].data.range) : null; let fragment = document.createDocumentFragment(); - let isEmptyHoverContent = true; let containColorPicker = false; - const markdownDisposeables = new DisposableStore(); + const disposables = new DisposableStore(); const markerMessages: MarkerHover[] = []; - messages.forEach((msg) => { + const markdownParts: MarkdownHover[] = []; + messages.forEach((_msg) => { + const msg = _msg.data; if (!msg.range) { return; } @@ -464,46 +567,37 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._colorPicker = widget; this.showAt(range.getStartPosition(), range, this._shouldFocus); - this.updateContents(fragment); + this._updateContents(fragment); this._colorPicker.layout(); - this.renderDisposable.value = combinedDisposable(colorListener, colorChangeListener, widget, markdownDisposeables); + this._renderDisposable = combinedDisposable(colorListener, colorChangeListener, widget, disposables); }); } else { if (msg instanceof MarkerHover) { markerMessages.push(msg); - isEmptyHoverContent = false; } else { - msg.contents - .filter(contents => !isEmptyMarkdownString(contents)) - .forEach(contents => { - const markdownHoverElement = $('div.hover-row.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = markdownDisposeables.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService)); - markdownDisposeables.add(renderer.onDidRenderAsync(() => { - hoverContentsElement.className = 'hover-contents code-hover-contents'; - this._hover.onContentsChanged(); - })); - const renderedContents = markdownDisposeables.add(renderer.render(contents)); - hoverContentsElement.appendChild(renderedContents.element); - fragment.appendChild(markdownHoverElement); - isEmptyHoverContent = false; - }); + if (msg instanceof MarkdownHover) { + markdownParts.push(msg); + } } } }); - if (markerMessages.length) { - markerMessages.forEach(msg => fragment.appendChild(this.renderMarkerHover(msg))); - const markerHoverForStatusbar = markerMessages.length === 1 ? markerMessages[0] : markerMessages.sort((a, b) => MarkerSeverity.compare(a.marker.severity, b.marker.severity))[0]; - fragment.appendChild(this.renderMarkerStatusbar(markerHoverForStatusbar)); + if (markdownParts.length > 0) { + disposables.add(this._markdownHoverParticipant.renderHoverParts(markdownParts, fragment)); } + if (markerMessages.length) { + disposables.add(this._markerHoverParticipant.renderHoverParts(markerMessages, fragment)); + } + + this._renderDisposable = disposables; + // show - if (!containColorPicker && !isEmptyHoverContent) { + if (!containColorPicker && fragment.hasChildNodes()) { this.showAt(new Position(renderRange.startLineNumber, renderColumn), highlightRange, this._shouldFocus); - this.updateContents(fragment); + this._updateContents(fragment); } this._isChangingDecorations = true; @@ -514,170 +608,17 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._isChangingDecorations = false; } - private renderMarkerHover(markerHover: MarkerHover): HTMLElement { - const hoverElement = $('div.hover-row'); - const markerElement = dom.append(hoverElement, $('div.marker.hover-contents')); - const { source, message, code, relatedInformation } = markerHover.marker; - - this._editor.applyFontInfo(markerElement); - const messageElement = dom.append(markerElement, $('span')); - messageElement.style.whiteSpace = 'pre-wrap'; - messageElement.innerText = message; - - if (source || code) { - // Code has link - if (code && typeof code !== 'string') { - const sourceAndCodeElement = $('span'); - if (source) { - const sourceElement = dom.append(sourceAndCodeElement, $('span')); - sourceElement.innerText = source; - } - this._codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); - this._codeLink.setAttribute('href', code.target.toString()); - - this._codeLink.onclick = (e) => { - this._openerService.open(code.target, { allowCommands: true }); - e.preventDefault(); - e.stopPropagation(); - }; - - const codeElement = dom.append(this._codeLink, $('span')); - codeElement.innerText = code.value; - - const detailsElement = dom.append(markerElement, sourceAndCodeElement); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - } else { - const detailsElement = dom.append(markerElement, $('span')); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; - } - } - - if (isNonEmptyArray(relatedInformation)) { - for (const { message, resource, startLineNumber, startColumn } of relatedInformation) { - const relatedInfoContainer = dom.append(markerElement, $('div')); - relatedInfoContainer.style.marginTop = '8px'; - const a = dom.append(relatedInfoContainer, $('a')); - a.innerText = `${basename(resource)}(${startLineNumber}, ${startColumn}): `; - a.style.cursor = 'pointer'; - a.onclick = e => { - e.stopPropagation(); - e.preventDefault(); - if (this._openerService) { - this._openerService.open(resource.with({ fragment: `${startLineNumber},${startColumn}` }), { fromUserGesture: true }).catch(onUnexpectedError); - } - }; - const messageElement = dom.append(relatedInfoContainer, $('span')); - messageElement.innerText = message; - this._editor.applyFontInfo(messageElement); - } - } - - return hoverElement; - } - - private renderMarkerStatusbar(markerHover: MarkerHover): HTMLElement { - const hoverElement = $('div.hover-row.status-bar'); - const disposables = new DisposableStore(); - const actionsElement = dom.append(hoverElement, $('div.actions')); - if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { - disposables.add(this._renderAction(actionsElement, { - label: nls.localize('peek problem', "Peek Problem"), - commandId: NextMarkerAction.ID, - run: () => { - this.hide(); - MarkerController.get(this._editor).showAtMarker(markerHover.marker); - this._editor.focus(); - } - })); - } - - if (!this._editor.getOption(EditorOption.readOnly)) { - const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); - quickfixPlaceholderElement.style.opacity = '0'; - quickfixPlaceholderElement.style.transition = 'opacity 0.2s'; - setTimeout(() => quickfixPlaceholderElement.style.opacity = '1', 200); - quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."); - disposables.add(toDisposable(() => quickfixPlaceholderElement.remove())); - - const codeActionsPromise = this.getCodeActions(markerHover.marker); - disposables.add(toDisposable(() => codeActionsPromise.cancel())); - codeActionsPromise.then(actions => { - quickfixPlaceholderElement.style.transition = ''; - quickfixPlaceholderElement.style.opacity = '1'; - - if (!actions.validActions.length) { - actions.dispose(); - quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); - return; - } - quickfixPlaceholderElement.remove(); - - let showing = false; - disposables.add(toDisposable(() => { - if (!showing) { - actions.dispose(); - } - })); - - disposables.add(this._renderAction(actionsElement, { - label: nls.localize('quick fixes', "Quick Fix..."), - commandId: QuickFixAction.Id, - run: (target) => { - showing = true; - const controller = QuickFixController.get(this._editor); - const elementPosition = dom.getDomNodePagePosition(target); - // Hide the hover pre-emptively, otherwise the editor can close the code actions - // context menu as well when using keyboard navigation - this.hide(); - controller.showCodeActions(markerCodeActionTrigger, actions, { - x: elementPosition.left + 6, - y: elementPosition.top + elementPosition.height + 6 - }); - } - })); - }); - } - - this.renderDisposable.value = disposables; - return hoverElement; - } - - private getCodeActions(marker: IMarker): CancelablePromise { - return createCancelablePromise(cancellationToken => { - return getCodeActions( - this._editor.getModel()!, - new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), - markerCodeActionTrigger, - Progress.None, - cancellationToken); - }); - } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ className: 'hoverHighlight' }); } -function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean { - if ((!first && second) || (first && !second) || first.length !== second.length) { +function hoverContentsEquals(first: HoverPartInfo[], second: HoverPartInfo[]): boolean { + if (first.length !== second.length) { return false; } for (let i = 0; i < first.length; i++) { - const firstElement = first[i]; - const secondElement = second[i]; - if (firstElement instanceof MarkerHover && secondElement instanceof MarkerHover) { - return IMarkerData.makeKey(firstElement.marker) === IMarkerData.makeKey(secondElement.marker); - } - if (firstElement instanceof ColorHover || secondElement instanceof ColorHover) { - return false; - } - if (firstElement instanceof MarkerHover || secondElement instanceof MarkerHover) { - return false; - } - if (!markedStringsEquals(firstElement.contents, secondElement.contents)) { + if (!first[i].data.equals(second[i].data)) { return false; } } diff --git a/src/vs/editor/contrib/indentation/indentation.ts b/src/vs/editor/contrib/indentation/indentation.ts index 9e081eeb89..3295f72acc 100644 --- a/src/vs/editor/contrib/indentation/indentation.ts +++ b/src/vs/editor/contrib/indentation/indentation.ts @@ -472,7 +472,6 @@ export class AutoIndentOnPaste implements IEditorContribution { } const autoIndent = this.editor.getOption(EditorOption.autoIndent); const { tabSize, indentSize, insertSpaces } = model.getOptions(); - this.editor.pushUndoStop(); let textEdits: TextEdit[] = []; let indentConverter = { @@ -583,9 +582,12 @@ export class AutoIndentOnPaste implements IEditorContribution { } } - let cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!); - this.editor.executeCommand('autoIndentOnPaste', cmd); - this.editor.pushUndoStop(); + if (textEdits.length > 0) { + this.editor.pushUndoStop(); + let cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!); + this.editor.executeCommand('autoIndentOnPaste', cmd); + this.editor.pushUndoStop(); + } } private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { diff --git a/src/vs/editor/contrib/inlineHints/inlineHintsController.ts b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts new file mode 100644 index 0000000000..eaab644342 --- /dev/null +++ b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { hash } from 'vs/base/common/hash'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IContentDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { InlineHintsProvider, InlineHintsProviderRegistry, InlineHint } from 'vs/editor/common/modes'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { flatten } from 'vs/base/common/arrays'; +import { editorInlineHintForeground, editorInlineHintBackground } from 'vs/platform/theme/common/colorRegistry'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Range } from 'vs/editor/common/core/range'; +import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { URI } from 'vs/base/common/uri'; +import { IRange } from 'vs/base/common/range'; +import { assertType } from 'vs/base/common/types'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +const MAX_DECORATORS = 500; + +export interface InlineHintsData { + list: InlineHint[]; + provider: InlineHintsProvider; +} + +export async function getInlineHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { + const datas: InlineHintsData[] = []; + const providers = InlineHintsProviderRegistry.ordered(model).reverse(); + const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlineHints(model, range, token)).then(result => { + if (result) { + datas.push({ list: result, provider }); + } + }, err => { + onUnexpectedExternalError(err); + })))); + + await Promise.all(promises); + + return datas; +} + +export class InlineHintsController implements IEditorContribution { + + static readonly ID: string = 'editor.contrib.InlineHints'; + + // static get(editor: ICodeEditor): InlineHintsController { + // return editor.getContribution(this.ID); + // } + + private readonly _disposables = new DisposableStore(); + private readonly _sessionDisposables = new DisposableStore(); + private readonly _getInlineHintsDelays = new LanguageFeatureRequestDelays(InlineHintsProviderRegistry, 250, 2500); + + private _decorationsTypeIds: string[] = []; + private _decorationIds: string[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IThemeService private readonly _themeService: IThemeService, + ) { + this._disposables.add(InlineHintsProviderRegistry.onDidChange(() => this._update())); + this._disposables.add(_themeService.onDidColorThemeChange(() => this._update())); + this._disposables.add(_editor.onDidChangeModel(() => this._update())); + this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); + this._disposables.add(_editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.inlineHints)) { + this._update(); + } + })); + + this._update(); + } + + dispose(): void { + this._sessionDisposables.dispose(); + this._removeAllDecorations(); + this._disposables.dispose(); + } + + private _update(): void { + this._sessionDisposables.clear(); + + if (!this._editor.getOption(EditorOption.inlineHints).enabled) { + this._removeAllDecorations(); + return; + } + + const model = this._editor.getModel(); + if (!model || !InlineHintsProviderRegistry.has(model)) { + this._removeAllDecorations(); + return; + } + + const scheduler = new RunOnceScheduler(async () => { + const t1 = Date.now(); + + const cts = new CancellationTokenSource(); + this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); + + const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); + const result = await getInlineHints(model, visibleRanges, cts.token); + + // update moving average + const newDelay = this._getInlineHintsDelays.update(model, Date.now() - t1); + scheduler.delay = newDelay; + + // render hints + this._updateHintsDecorators(result); + + }, this._getInlineHintsDelays.get(model)); + + this._sessionDisposables.add(scheduler); + + // update inline hints when content or scroll position changes + this._sessionDisposables.add(this._editor.onDidChangeModelContent(() => scheduler.schedule())); + this._disposables.add(this._editor.onDidScrollChange(() => scheduler.schedule())); + scheduler.schedule(); + + // update inline hints when any any provider fires an event + const providerListener = new DisposableStore(); + this._sessionDisposables.add(providerListener); + for (const provider of InlineHintsProviderRegistry.all(model)) { + if (typeof provider.onDidChangeInlineHints === 'function') { + providerListener.add(provider.onDidChangeInlineHints(() => scheduler.schedule())); + } + } + } + + private _updateHintsDecorators(hintsData: InlineHintsData[]): void { + const { fontSize, fontFamily } = this._getLayoutInfo(); + const backgroundColor = this._themeService.getColorTheme().getColor(editorInlineHintBackground); + const fontColor = this._themeService.getColorTheme().getColor(editorInlineHintForeground); + + const newDecorationsTypeIds: string[] = []; + const newDecorationsData: IModelDeltaDecoration[] = []; + + for (const { list: hints } of hintsData) { + + for (let j = 0; j < hints.length && newDecorationsData.length < MAX_DECORATORS; j++) { + const { text, range, description: hoverMessage, whitespaceBefore, whitespaceAfter } = hints[j]; + const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; + const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; + + const before: IContentDecorationRenderOptions = { + contentText: text, + backgroundColor: `${backgroundColor}`, + color: `${fontColor}`, + margin: `0px ${marginAfter}px 0px ${marginBefore}px`, + fontSize: `${fontSize}px`, + fontFamily: fontFamily, + padding: `0px ${(fontSize / 4) | 0}px`, + borderRadius: `${(fontSize / 4) | 0}px`, + }; + const key = 'inlineHints-' + hash(before).toString(16); + this._codeEditorService.registerDecorationType(key, { before }, undefined, this._editor); + + // decoration types are ref-counted which means we only need to + // call register und remove equally often + newDecorationsTypeIds.push(key); + + const options = this._codeEditorService.resolveDecorationOptions(key, true); + if (typeof hoverMessage === 'string') { + options.hoverMessage = new MarkdownString().appendText(hoverMessage); + } else if (hoverMessage) { + options.hoverMessage = hoverMessage; + } + + newDecorationsData.push({ + range, + options + }); + } + } + + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = newDecorationsTypeIds; + + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, newDecorationsData); + } + + private _getLayoutInfo() { + const options = this._editor.getOption(EditorOption.inlineHints); + const editorFontSize = this._editor.getOption(EditorOption.fontSize); + let fontSize = options.fontSize; + if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { + fontSize = (editorFontSize * .9) | 0; + } + const fontFamily = options.fontFamily; + return { fontSize, fontFamily }; + } + + private _removeAllDecorations(): void { + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, []); + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = []; + } +} + +registerEditorContribution(InlineHintsController.ID, InlineHintsController); + +CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { + + const [uri, range] = args; + assertType(URI.isUri(uri)); + assertType(Range.isIRange(range)); + + const ref = await accessor.get(ITextModelService).createModelReference(uri); + try { + const data = await getInlineHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); + return flatten(data.map(item => item.list)).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + } finally { + ref.dispose(); + } +}); diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 15a2767a9d..bb53b2b0fb 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -939,43 +939,39 @@ export class TransposeAction extends EditorAction { export abstract class AbstractCaseAction extends EditorAction { public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - let selections = editor.getSelections(); + const selections = editor.getSelections(); if (selections === null) { return; } - let model = editor.getModel(); + const model = editor.getModel(); if (model === null) { return; } - let wordSeparators = editor.getOption(EditorOption.wordSeparators); + const wordSeparators = editor.getOption(EditorOption.wordSeparators); + const textEdits: IIdentifiedSingleEditOperation[] = []; - let commands: ICommand[] = []; - - for (let i = 0, len = selections.length; i < len; i++) { - let selection = selections[i]; + for (const selection of selections) { if (selection.isEmpty()) { - let cursor = selection.getStartPosition(); + const cursor = selection.getStartPosition(); const word = editor.getConfiguredWordAtPosition(cursor); if (!word) { continue; } - let wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); - let text = model.getValueInRange(wordRange); - commands.push(new ReplaceCommandThatPreservesSelection(wordRange, this._modifyText(text, wordSeparators), - new Selection(cursor.lineNumber, cursor.column, cursor.lineNumber, cursor.column))); - + const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn); + const text = model.getValueInRange(wordRange); + textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators))); } else { - let text = model.getValueInRange(selection); - commands.push(new ReplaceCommandThatPreservesSelection(selection, this._modifyText(text, wordSeparators), selection)); + const text = model.getValueInRange(selection); + textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators))); } } editor.pushUndoStop(); - editor.executeCommands(this.id, commands); + editor.executeEdits(this.id, textEdits); editor.pushUndoStop(); } @@ -1049,6 +1045,25 @@ export class TitleCaseAction extends AbstractCaseAction { } } +export class SnakeCaseAction extends AbstractCaseAction { + constructor() { + super({ + id: 'editor.action.transformToSnakecase', + label: nls.localize('editor.transformToSnakecase', "Transform to Snake Case"), + alias: 'Transform to Snake Case', + precondition: EditorContextKeys.writable + }); + } + + protected _modifyText(text: string, wordSeparators: string): string { + return (text + .replace(/(\p{Ll})(\p{Lu})/gmu, '$1_$2') + .replace(/([^\b_])(\p{Lu})(\p{Ll})/gmu, '$1_$2$3') + .toLocaleLowerCase() + ); + } +} + registerEditorAction(CopyLinesUpAction); registerEditorAction(CopyLinesDownAction); registerEditorAction(DuplicateSelectionAction); @@ -1069,3 +1084,4 @@ registerEditorAction(TransposeAction); registerEditorAction(UpperCaseAction); registerEditorAction(LowerCaseAction); registerEditorAction(TitleCaseAction); +registerEditorAction(SnakeCaseAction); diff --git a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts index 7ab636e1ff..85cb6314a1 100644 --- a/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/moveLinesCommand.ts @@ -299,13 +299,13 @@ export class MoveLinesCommand implements ICommand { } } - private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, oneLineAboveText?: string) { + private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) { let validPrecedingLine = oneLineAbove; while (validPrecedingLine >= 1) { // ship empty lines as empty lines just inherit indentation let lineContent; - if (validPrecedingLine === oneLineAbove && oneLineAboveText !== undefined) { - lineContent = oneLineAboveText; + if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) { + lineContent = previousLineText; } else { lineContent = model.getLineContent(validPrecedingLine); } diff --git a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts index 7b3d711b5e..04e00691a8 100644 --- a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts @@ -207,8 +207,8 @@ suite('Editor Contrib - Duplicate Selection', () => { withTestCodeEditor(lines.join('\n'), {}, (editor) => { editor.setSelections(selections); duplicateSelectionAction.run(null!, editor, {}); - assert.deepEqual(editor.getValue(), expectedLines.join('\n')); - assert.deepEqual(editor.getSelections()!.map(s => s.toString()), expectedSelections.map(s => s.toString())); + assert.deepStrictEqual(editor.getValue(), expectedLines.join('\n')); + assert.deepStrictEqual(editor.getSelections()!.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); } diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 23ac1387bf..013ad69f92 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -8,7 +8,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction } from 'vs/editor/contrib/linesOperations/linesOperations'; +import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction, SnakeCaseAction } from 'vs/editor/contrib/linesOperations/linesOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -19,7 +19,7 @@ function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]) if (!Array.isArray(expected)) { expected = [expected]; } - assert.deepEqual(editor.getSelections(), expected); + assert.deepStrictEqual(editor.getSelections(), expected); } function executeAction(action: EditorAction, editor: ICodeEditor): void { @@ -40,7 +40,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 3, 5)); executeAction(sortLinesAscendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron' @@ -65,7 +65,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 1, 3, 5), new Selection(5, 1, 7, 5)]); executeAction(sortLinesAscendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron', @@ -79,7 +79,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(5, 1, 7, 7) ]; editor.getSelections()!.forEach((actualSelection, index) => { - assert.deepEqual(actualSelection.toString(), expectedSelections[index].toString()); + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); @@ -98,7 +98,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 3, 7)); executeAction(sortLinesDescendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha' @@ -123,7 +123,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 1, 3, 7), new Selection(5, 1, 7, 7)]); executeAction(sortLinesDescendingAction, editor); - assert.deepEqual(model.getLinesContent(), [ + assert.deepStrictEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha', @@ -137,7 +137,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(5, 1, 7, 5) ]; editor.getSelections()!.forEach((actualSelection, index) => { - assert.deepEqual(actualSelection.toString(), expectedSelections[index].toString()); + assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); @@ -157,12 +157,12 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 2)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'ne'); + assert.strictEqual(model.getLineContent(1), 'ne'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(3, 2, 3, 2)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(2), 'wo'); - assert.equal(model.getLineContent(3), 'hree'); + assert.strictEqual(model.getLineContent(2), 'wo'); + assert.strictEqual(model.getLineContent(3), 'hree'); }); }); @@ -178,16 +178,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'onetwo'); + assert.strictEqual(model.getLineContent(1), 'onetwo'); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLinesContent()[0], 'onetwothree'); - assert.equal(model.getLinesContent().length, 1); + assert.strictEqual(model.getLinesContent()[0], 'onetwothree'); + assert.strictEqual(model.getLinesContent().length, 1); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLinesContent()[0], 'onetwothree'); + assert.strictEqual(model.getLinesContent()[0], 'onetwothree'); }); }); @@ -213,25 +213,25 @@ suite('Editor Contrib - Line Operations', () => { executeAction(deleteAllLeftAction, editor); let selections = editor.getSelections()!; - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' waso waso'); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' waso waso'); + assert.strictEqual(model.getLineContent(5), ''); - assert.deepEqual([ + assert.deepStrictEqual([ selections[0].startLineNumber, selections[0].startColumn, selections[0].endLineNumber, selections[0].endColumn ], [3, 1, 3, 1]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[1].startLineNumber, selections[1].startColumn, selections[1].endLineNumber, selections[1].endColumn ], [2, 1, 2, 1]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[2].startLineNumber, selections[2].startColumn, selections[2].endLineNumber, @@ -241,17 +241,17 @@ suite('Editor Contrib - Line Operations', () => { executeAction(deleteAllLeftAction, editor); selections = editor.getSelections()!; - assert.equal(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); - assert.equal(selections.length, 2); + assert.strictEqual(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); + assert.strictEqual(selections.length, 2); - assert.deepEqual([ + assert.deepStrictEqual([ selections[0].startLineNumber, selections[0].startColumn, selections[0].endLineNumber, selections[0].endColumn ], [1, 27, 1, 27]); - assert.deepEqual([ + assert.deepStrictEqual([ selections[1].startLineNumber, selections[1].startColumn, selections[1].endLineNumber, @@ -277,23 +277,23 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([new Selection(1, 2, 1, 2), new Selection(1, 4, 1, 4)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'lo'); + assert.strictEqual(model.getLineContent(1), 'lo'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(2, 4, 2, 5)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(2), 'd'); + assert.strictEqual(model.getLineContent(2), 'd'); editor.setSelections([new Selection(3, 2, 3, 5), new Selection(3, 7, 3, 7)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(3), 'world'); + assert.strictEqual(model.getLineContent(3), 'world'); editor.setSelections([new Selection(4, 3, 4, 3), new Selection(4, 5, 5, 4)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(4), 'jour'); + assert.strictEqual(model.getLineContent(4), 'jour'); editor.setSelections([new Selection(5, 3, 6, 3), new Selection(6, 5, 7, 5), new Selection(7, 7, 7, 7)]); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(5), 'world'); + assert.strictEqual(model.getLineContent(5), 'world'); }); }); @@ -310,16 +310,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); editor.trigger('keyboard', Handler.Type, { text: 'Typing some text here on line ' }); - assert.equal(model.getLineContent(1), 'Typing some text here on line one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); + assert.strictEqual(model.getLineContent(1), 'Typing some text here on line one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); executeAction(deleteAllLeftAction, editor); - assert.equal(model.getLineContent(1), 'one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); + assert.strictEqual(model.getLineContent(1), 'one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Typing some text here on line one'); - assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); + assert.strictEqual(model.getLineContent(1), 'Typing some text here on line one'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); }); }); }); @@ -345,27 +345,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 6, 1, 6)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(2), 'hello world'); + assert.strictEqual(model.getLineContent(2), 'hello world'); assertSelection(editor, new Selection(2, 7, 2, 7)); editor.setSelection(new Selection(3, 2, 3, 2)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(3), 'hello world'); + assert.strictEqual(model.getLineContent(3), 'hello world'); assertSelection(editor, new Selection(3, 7, 3, 7)); editor.setSelection(new Selection(4, 2, 5, 3)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(4), 'hello world'); + assert.strictEqual(model.getLineContent(4), 'hello world'); assertSelection(editor, new Selection(4, 2, 4, 8)); editor.setSelection(new Selection(5, 1, 7, 3)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(5), 'hello world'); + assert.strictEqual(model.getLineContent(5), 'hello world'); assertSelection(editor, new Selection(5, 1, 5, 3)); }); }); @@ -381,8 +381,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello'); - assert.equal(model.getLineContent(2), 'world'); + assert.strictEqual(model.getLineContent(1), 'hello'); + assert.strictEqual(model.getLineContent(2), 'world'); assertSelection(editor, new Selection(2, 6, 2, 6)); }); }); @@ -416,7 +416,7 @@ suite('Editor Contrib - Line Operations', () => { ]); executeAction(joinLinesAction, editor); - assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world'); + assert.strictEqual(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world'); assertSelection(editor, [ /** primary cursor */ new Selection(3, 4, 3, 8), @@ -440,16 +440,16 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 6, 1, 6)); editor.trigger('keyboard', Handler.Type, { text: ' my dear' }); - assert.equal(model.getLineContent(1), 'hello my dear'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); executeAction(joinLinesAction, editor); - assert.equal(model.getLineContent(1), 'hello my dear world'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear world'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'hello my dear'); - assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); + assert.strictEqual(model.getLineContent(1), 'hello my dear'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); }); }); }); @@ -467,27 +467,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 2, 1, 2)); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hell oworld'); + assert.strictEqual(model.getLineContent(1), 'hell oworld'); assertSelection(editor, new Selection(1, 7, 1, 7)); editor.setSelection(new Selection(1, 12, 1, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(1), 'hell oworl'); + assert.strictEqual(model.getLineContent(1), 'hell oworl'); assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(3, 1, 3, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(3), ''); assertSelection(editor, new Selection(4, 1, 4, 1)); editor.setSelection(new Selection(4, 2, 4, 2)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(4), ' '); assertSelection(editor, new Selection(4, 3, 4, 3)); } ); @@ -509,22 +509,22 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(2), ''); assertSelection(editor, new Selection(2, 1, 2, 1)); editor.setSelection(new Selection(3, 6, 3, 6)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(4), 'oworld'); + assert.strictEqual(model.getLineContent(4), 'oworld'); assertSelection(editor, new Selection(4, 2, 4, 2)); editor.setSelection(new Selection(6, 12, 6, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(7), 'd'); + assert.strictEqual(model.getLineContent(7), 'd'); assertSelection(editor, new Selection(7, 2, 7, 2)); editor.setSelection(new Selection(8, 12, 8, 12)); executeAction(transposeAction, editor); - assert.equal(model.getLineContent(8), 'hello world'); + assert.strictEqual(model.getLineContent(8), 'hello world'); assertSelection(editor, new Selection(8, 12, 8, 12)); } ); @@ -534,52 +534,131 @@ suite('Editor Contrib - Line Operations', () => { withTestCodeEditor( [ 'hello world', - 'öçşğü' + 'öçşğü', + 'parseHTMLString', + 'getElementById', + 'insertHTML', + 'PascalCase', + 'CSSSelectorsList', + 'iD', + 'tEST', + 'öçşÖÇŞğüĞÜ', + 'audioConverter.convertM4AToMP3();', + 'snake_case', + 'Capital_Snake_Case', + `function helloWorld() { + return someGlobalObject.printHelloWorld("en", "utf-8"); + } + helloWorld();`.replace(/^\s+/gm, '') ], {}, (editor) => { let model = editor.getModel()!; let uppercaseAction = new UpperCaseAction(); let lowercaseAction = new LowerCaseAction(); let titlecaseAction = new TitleCaseAction(); + let snakecaseAction = new SnakeCaseAction(); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), 'HELLO WORLD'); + assert.strictEqual(model.getLineContent(1), 'HELLO WORLD'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 3, 1, 3)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), 'HELLO world'); + assert.strictEqual(model.getLineContent(1), 'HELLO world'); assertSelection(editor, new Selection(1, 3, 1, 3)); editor.setSelection(new Selection(1, 4, 1, 4)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), 'hello world'); + assert.strictEqual(model.getLineContent(1), 'hello world'); assertSelection(editor, new Selection(1, 4, 1, 4)); editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(1), 'Hello World'); + assert.strictEqual(model.getLineContent(1), 'Hello World'); assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ'); + assert.strictEqual(model.getLineContent(2), 'ÖÇŞĞÜ'); assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(2), 'öçşğü'); + assert.strictEqual(model.getLineContent(2), 'öçşğü'); assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(2), 'Öçşğü'); + assert.strictEqual(model.getLineContent(2), 'Öçşğü'); assertSelection(editor, new Selection(2, 1, 2, 6)); + + editor.setSelection(new Selection(3, 1, 3, 16)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(3), 'parse_html_string'); + assertSelection(editor, new Selection(3, 1, 3, 18)); + + editor.setSelection(new Selection(4, 1, 4, 15)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(4), 'get_element_by_id'); + assertSelection(editor, new Selection(4, 1, 4, 18)); + + editor.setSelection(new Selection(5, 1, 5, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(5), 'insert_html'); + assertSelection(editor, new Selection(5, 1, 5, 12)); + + editor.setSelection(new Selection(6, 1, 6, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(6), 'pascal_case'); + assertSelection(editor, new Selection(6, 1, 6, 12)); + + editor.setSelection(new Selection(7, 1, 7, 17)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(7), 'css_selectors_list'); + assertSelection(editor, new Selection(7, 1, 7, 19)); + + editor.setSelection(new Selection(8, 1, 8, 3)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(8), 'i_d'); + assertSelection(editor, new Selection(8, 1, 8, 4)); + + editor.setSelection(new Selection(9, 1, 9, 5)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(9), 't_est'); + assertSelection(editor, new Selection(9, 1, 9, 6)); + + editor.setSelection(new Selection(10, 1, 10, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(10), 'öçş_öç_şğü_ğü'); + assertSelection(editor, new Selection(10, 1, 10, 14)); + + editor.setSelection(new Selection(11, 1, 11, 34)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(11), 'audio_converter.convert_m4a_to_mp3();'); + assertSelection(editor, new Selection(11, 1, 11, 38)); + + editor.setSelection(new Selection(12, 1, 12, 11)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(12), 'snake_case'); + assertSelection(editor, new Selection(12, 1, 12, 11)); + + editor.setSelection(new Selection(13, 1, 13, 19)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getLineContent(13), 'capital_snake_case'); + assertSelection(editor, new Selection(13, 1, 13, 19)); + + editor.setSelection(new Selection(14, 1, 17, 14)); + executeAction(snakecaseAction, editor); + assert.strictEqual(model.getValueInRange(new Selection(14, 1, 17, 15)), `function hello_world() { + return some_global_object.print_hello_world("en", "utf-8"); + } + hello_world();`.replace(/^\s+/gm, '')); + assertSelection(editor, new Selection(14, 1, 17, 15)); } ); @@ -597,27 +676,27 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(1), 'Foo Bar Baz'); + assert.strictEqual(model.getLineContent(1), 'Foo Bar Baz'); editor.setSelection(new Selection(2, 1, 2, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(2), 'Foo\'Bar\'Baz'); + assert.strictEqual(model.getLineContent(2), 'Foo\'Bar\'Baz'); editor.setSelection(new Selection(3, 1, 3, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(3), 'Foo[Bar]Baz'); + assert.strictEqual(model.getLineContent(3), 'Foo[Bar]Baz'); editor.setSelection(new Selection(4, 1, 4, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(4), 'Foo`Bar~Baz'); + assert.strictEqual(model.getLineContent(4), 'Foo`Bar~Baz'); editor.setSelection(new Selection(5, 1, 5, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(5), 'Foo^Bar%Baz'); + assert.strictEqual(model.getLineContent(5), 'Foo^Bar%Baz'); editor.setSelection(new Selection(6, 1, 6, 12)); executeAction(titlecaseAction, editor); - assert.equal(model.getLineContent(6), 'Foo$Bar!Baz'); + assert.strictEqual(model.getLineContent(6), 'Foo$Bar!Baz'); } ); @@ -632,22 +711,22 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(uppercaseAction, editor); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(2), ' '); assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(2, 2, 2, 2)); executeAction(lowercaseAction, editor); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(2), ' '); assertSelection(editor, new Selection(2, 2, 2, 2)); } ); @@ -660,18 +739,18 @@ suite('Editor Contrib - Line Operations', () => { const action = new DeleteAllRightAction(); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 1)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(1, 1, 1, 1), new Selection(1, 1, 1, 1)]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); }); @@ -685,18 +764,18 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 2, 1, 5)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['ho', 'world']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); + assert.deepStrictEqual(model.getLinesContent(), ['ho', 'world']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); editor.setSelection(new Selection(1, 1, 2, 4)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['ld']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['ld']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 3)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); }); @@ -710,13 +789,13 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 3, 1, 3)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', 'world']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); + assert.deepStrictEqual(model.getLinesContent(), ['he', 'world']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); editor.setSelection(new Selection(2, 1, 2, 1)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', '']); - assert.deepEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); + assert.deepStrictEqual(model.getLinesContent(), ['he', '']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); }); }); @@ -730,18 +809,18 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['helloworld']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['helloworld']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hello']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['hello']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hello']); - assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); + assert.deepStrictEqual(model.getLinesContent(), ['hello']); + assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); }); @@ -760,35 +839,35 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hethere', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hewor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hewor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['he']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['he']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); }); @@ -809,20 +888,20 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); executeAction(action, editor); - assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(model.getLinesContent(), ['hethere', 'wor']); + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6), new Selection(3, 4, 3, 4) ]); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); @@ -847,27 +926,27 @@ suite('Editor Contrib - Line Operations', () => { } testInsertLineBefore(1, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); - assert.equal(model.getLineContent(1), ''); - assert.equal(model.getLineContent(2), 'First line'); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); + assert.strictEqual(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(2), 'First line'); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineBefore(2, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineBefore(3, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); }); @@ -888,27 +967,27 @@ suite('Editor Contrib - Line Operations', () => { } testInsertLineAfter(1, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), 'Second line'); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'Second line'); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineAfter(2, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), 'Third line'); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), 'Third line'); }); testInsertLineAfter(3, 3, (model, viewModel) => { - assert.deepEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); - assert.equal(model.getLineContent(1), 'First line'); - assert.equal(model.getLineContent(2), 'Second line'); - assert.equal(model.getLineContent(3), 'Third line'); - assert.equal(model.getLineContent(4), ''); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); + assert.strictEqual(model.getLineContent(1), 'First line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(3), 'Third line'); + assert.strictEqual(model.getLineContent(4), ''); }); }); @@ -928,11 +1007,11 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 2)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), '\tfunction baz() {'); - assert.deepEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); + assert.strictEqual(model.getLineContent(1), '\tfunction baz() {'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), '\tf\tunction baz() {'); + assert.strictEqual(model.getLineContent(1), '\tf\tunction baz() {'); }); model.dispose(); @@ -953,8 +1032,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 1)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), '\tSome text'); - assert.deepEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); + assert.strictEqual(model.getLineContent(1), '\tSome text'); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); }); model.dispose(); @@ -972,8 +1051,8 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(1, 1)); executeAction(indentLinesAction, editor); - assert.equal(model.getLineContent(1), ' '); - assert.deepEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); + assert.strictEqual(model.getLineContent(1), ' '); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); }); model.dispose(); @@ -995,7 +1074,7 @@ suite('Editor Contrib - Line Operations', () => { const deleteLinesAction = new DeleteLinesAction(); executeAction(deleteLinesAction, editor); - assert.equal(editor.getValue(), 'a\nc'); + assert.strictEqual(editor.getValue(), 'a\nc'); }); }); @@ -1007,8 +1086,8 @@ suite('Editor Contrib - Line Operations', () => { const deleteLinesAction = new DeleteLinesAction(); executeAction(deleteLinesAction, editor); - assert.equal(editor.getValue(), resultingText.join('\n')); - assert.deepEqual(editor.getSelections(), resultingSelections); + assert.strictEqual(editor.getValue(), resultingText.join('\n')); + assert.deepStrictEqual(editor.getSelections(), resultingSelections); }); } diff --git a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts index dcf34b8359..1648cd8b01 100644 --- a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts +++ b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts @@ -220,6 +220,7 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont } try { + this._editor.popUndoStop(); this._ignoreChangeEvent = true; const prevEditOperationType = this._editor._getViewModel().getPrevEditOperationType(); this._editor.executeEdits('linkedEditing', edits); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 085591a1f7..4709435e60 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -57,7 +57,7 @@ function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { nativeLabel = ` "${nativeLabelText}"`; } } - const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString()}${nativeLabel}) (${kb})`); + const hoverMessage = new MarkdownString('', true).appendMarkdown(`[${label}](${link.url.toString(true)}${nativeLabel}) (${kb})`); return hoverMessage; } else { return new MarkdownString().appendText(`${label} (${kb})`); diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 827c96f1d8..10e8eaeffa 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -850,7 +850,6 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this.updateSoon.schedule(); } else { this._setState(null); - } } else { this._update(); @@ -1016,11 +1015,6 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }); this.decorations = this.editor.deltaDecorations(this.decorations, decorations); - - const currentFindState = CommonFindController.get(this.editor).getState(); - if (currentFindState.isRegex || currentFindState.matchCase || currentFindState.wholeWord) { - CommonFindController.get(this.editor).highlightFindOptions(true); - } } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ diff --git a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts index 33d1170aa0..ebefb3cdb2 100644 --- a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts @@ -25,7 +25,7 @@ suite('Multicursor', () => { editor.setSelection(new Selection(2, 1, 2, 1)); addCursorUpAction.run(null!, editor, {}); - assert.equal(viewModel.getSelections().length, 2); + assert.strictEqual(viewModel.getSelections().length, 2); editor.trigger('test', Handler.Paste, { text: '1\n2', @@ -35,8 +35,8 @@ suite('Multicursor', () => { ] }); - assert.equal(editor.getModel()!.getLineContent(1), '1abc'); - assert.equal(editor.getModel()!.getLineContent(2), '2def'); + assert.strictEqual(editor.getModel()!.getLineContent(1), '1abc'); + assert.strictEqual(editor.getModel()!.getLineContent(2), '2def'); }); }); @@ -46,7 +46,7 @@ suite('Multicursor', () => { ], {}, (editor, viewModel) => { let addCursorDownAction = new InsertCursorBelow(); addCursorDownAction.run(null!, editor, {}); - assert.equal(viewModel.getSelections().length, 1); + assert.strictEqual(viewModel.getSelections().length, 1); }); }); @@ -90,7 +90,7 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 9, 2, 16)); selectHighlightsAction.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 9, 2, 16], [1, 9, 1, 16], [3, 9, 3, 16], @@ -98,7 +98,7 @@ suite('Multicursor selection', () => { editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 9, 2, 16]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 9, 2, 16]); multiCursorSelectController.dispose(); findController.dispose(); @@ -121,13 +121,13 @@ suite('Multicursor selection', () => { findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false); selectHighlightsAction.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 10], [2, 1, 2, 11], [3, 1, 3, 12], ]); - assert.equal(findController.getState().searchString, 'some+thing'); + assert.strictEqual(findController.getState().searchString, 'some+thing'); multiCursorSelectController.dispose(); findController.dispose(); @@ -154,14 +154,14 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 1, 3, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 1, 3, 4], [8, 1, 9, 4] ]); editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); multiCursorSelectController.dispose(); findController.dispose(); @@ -182,7 +182,7 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(1, 1, 1, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 4], [1, 4, 1, 7] ]); @@ -190,7 +190,7 @@ suite('Multicursor selection', () => { addSelectionToNextFindMatch.run(null!, editor); addSelectionToNextFindMatch.run(null!, editor); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 1, 1, 4], [1, 4, 1, 7], [2, 1, 2, 4], @@ -199,14 +199,14 @@ suite('Multicursor selection', () => { ]); editor.trigger('test', Handler.Type, { text: 'z' }); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [1, 2, 1, 2], [1, 3, 1, 3], [2, 2, 2, 2], [3, 2, 3, 2], [3, 3, 3, 3] ]); - assert.equal(editor.getValue(), [ + assert.strictEqual(editor.getValue(), [ 'zz', 'z', 'zz', @@ -239,14 +239,14 @@ suite('Multicursor selection', () => { editor.setSelection(new Selection(2, 1, 3, 4)); addSelectionToNextFindMatch.run(null!, editor); - assert.deepEqual(editor.getSelections()!.map(fromRange), [ + assert.deepStrictEqual(editor.getSelections()!.map(fromRange), [ [2, 1, 3, 4], [8, 1, 9, 4] ]); editor.trigger('test', 'removeSecondaryCursors', null); - assert.deepEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); + assert.deepStrictEqual(fromRange(editor.getSelection()!), [2, 1, 3, 4]); multiCursorSelectController.dispose(); findController.dispose(); @@ -284,25 +284,25 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -323,20 +323,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -357,20 +357,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -392,14 +392,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), new Selection(3, 1, 3, 4), @@ -421,14 +421,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 5, 1, 10), new Selection(2, 5, 2, 10), new Selection(3, 5, 3, 8), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 5, 1, 10), new Selection(2, 5, 2, 10), new Selection(3, 5, 3, 8), @@ -450,20 +450,20 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -471,7 +471,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -480,7 +480,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), @@ -508,18 +508,18 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), @@ -534,12 +534,12 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), ]); @@ -550,7 +550,7 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(2, 1, 2, 4), ]); @@ -565,14 +565,14 @@ suite('Multicursor selection', () => { ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), ]); action.run(null!, editor); - assert.deepEqual(editor.getSelections(), [ + assert.deepStrictEqual(editor.getSelections(), [ new Selection(1, 1, 1, 4), new Selection(4, 1, 4, 4), new Selection(6, 2, 6, 5), diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index adb6ec6b11..320c82ca99 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -10,7 +10,7 @@ line-height: 1.5em; } -.monaco-editor .parameter-hints-widget > .wrapper { +.monaco-editor .parameter-hints-widget > .phwrapper { max-width: 440px; display: flex; flex-direction: row; diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index dbb5543190..0cd2fe3b09 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -14,7 +14,7 @@ import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentW import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as modes from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -27,6 +27,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { assertIsDefined } from 'vs/base/common/types'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; const $ = dom.$; @@ -81,7 +82,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { private createParamaterHintDOMNodes() { const element = $('.editor-widget.parameter-hints-widget'); - const wrapper = dom.append(element, $('.wrapper')); + const wrapper = dom.append(element, $('.phwrapper')); wrapper.tabIndex = -1; const controls = dom.append(wrapper, $('.controls')); @@ -225,8 +226,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { if (typeof activeParameter.documentation === 'string') { documentation.textContent = activeParameter.documentation; } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(activeParameter.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(activeParameter.documentation); documentation.appendChild(renderedContents.element); } dom.append(this.domNodes.docs, $('p', {}, documentation)); @@ -237,8 +237,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } else if (typeof signature.documentation === 'string') { dom.append(this.domNodes.docs, $('p', {}, signature.documentation)); } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(signature.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(signature.documentation); dom.append(this.domNodes.docs, renderedContents.element); } @@ -265,6 +264,16 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { this.domNodes.scrollbar.scanDomNode(); } + private renderMarkdownDocs(markdown: IMarkdownString | undefined): IMarkdownRenderResult { + const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(markdown, { + asyncRenderCallback: () => { + this.domNodes?.scrollbar.scanDomNode(); + } + })); + renderedContents.element.classList.add('markdown-docs'); + return renderedContents; + } + private hasDocs(signature: modes.SignatureInformation, activeParameter: modes.ParameterInformation | undefined): boolean { if (activeParameter && typeof activeParameter.documentation === 'string' && assertIsDefined(activeParameter.documentation).length > 0) { return true; @@ -360,7 +369,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { const height = Math.max(this.editor.getLayoutInfo().height / 4, 250); const maxHeight = `${height}px`; this.domNodes.element.style.maxHeight = maxHeight; - const wrapper = this.domNodes.element.getElementsByClassName('wrapper') as HTMLCollectionOf; + const wrapper = this.domNodes.element.getElementsByClassName('phwrapper') as HTMLCollectionOf; if (wrapper.length) { wrapper[0].style.maxHeight = maxHeight; } diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index fcc6d9c59a..39298929a7 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { first } from 'vs/base/common/async'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; @@ -20,19 +19,26 @@ export const Context = { MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp( +export async function provideSignatureHelp( model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken -): Promise { +): Promise { const supports = modes.SignatureHelpProviderRegistry.ordered(model); - return first(supports.map(support => () => { - return Promise.resolve(support.provideSignatureHelp(model, position, token, context)) - .catch(e => onUnexpectedExternalError(e)); - })); + for (const support of supports) { + try { + const result = await support.provideSignatureHelp(model, position, token, context); + if (result) { + return result; + } + } catch (err) { + onUnexpectedExternalError(err); + } + } + return undefined; } CommandsRegistry.registerCommand('_executeSignatureHelpProvider', async (accessor, ...args: [URI, IPosition, string?]) => { diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index fafd2d82cf..de83b5d958 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/peekViewWidget'; import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ActionBar, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionsOrientation, IActionBarOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; import { Color } from 'vs/base/common/color'; import { Emitter } from 'vs/base/common/event'; @@ -25,8 +25,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerColor, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Codicon } from 'vs/base/common/codicons'; -import { MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export const IPeekViewService = createDecorator('IPeekViewService'); export interface IPeekViewService { @@ -107,6 +106,7 @@ export abstract class PeekViewWidget extends ZoneWidget { private readonly _onDidClose = new Emitter(); readonly onDidClose = this._onDidClose.event; + private disposed?: true; protected _headElement?: HTMLDivElement; protected _primaryHeading?: HTMLElement; @@ -125,8 +125,11 @@ export abstract class PeekViewWidget extends ZoneWidget { } dispose(): void { - super.dispose(); - this._onDidClose.fire(this); + if (!this.disposed) { + this.disposed = true; // prevent consumers who dispose on onDidClose from looping + super.dispose(); + this._onDidClose.fire(this); + } } style(styles: IPeekViewStyles): void { @@ -204,15 +207,8 @@ export abstract class PeekViewWidget extends ZoneWidget { protected _getActionBarOptions(): IActionBarOptions { return { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; - } + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService), + orientation: ActionsOrientation.HORIZONTAL }; } diff --git a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts index be1db124ca..13e84b0cf3 100644 --- a/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/commandsQuickAccess.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { stripCodicons } from 'vs/base/common/codicons'; +import { stripIcons } from 'vs/base/common/iconLabels'; export abstract class AbstractEditorCommandsQuickAccessProvider extends AbstractCommandsQuickAccessProvider { @@ -41,7 +41,7 @@ export abstract class AbstractEditorCommandsQuickAccessProvider extends Abstract editorCommandPicks.push({ commandId: editorAction.id, commandAlias: editorAction.alias, - label: stripCodicons(editorAction.label) || editorAction.id, + label: stripIcons(editorAction.label) || editorAction.id, }); } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index e6f7d68873..2aed851f86 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -12,11 +12,10 @@ import { ITextModel } from 'vs/editor/common/model'; import { IRange, Range } from 'vs/editor/common/core/range'; import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { trim, format } from 'vs/base/common/strings'; import { prepareQuery, IPreparedQuery, pieceToQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer'; import { IMatch } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; import { Codicon } from 'vs/base/common/codicons'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { @@ -144,7 +143,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Resolve symbols from document once and reuse this // request for all filtering and typing then on - const symbolsPromise = this.getDocumentSymbols(model, true, token); + const symbolsPromise = this.getDocumentSymbols(model, token); // Set initial picks and update on type let picksCts: CancellationTokenSource | undefined = undefined; @@ -418,49 +417,9 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return result; } - protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { + protected async getDocumentSymbols(document: ITextModel, token: CancellationToken): Promise { const model = await OutlineModel.create(document, token); - if (token.isCancellationRequested) { - return []; - } - - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (flatten) { - this.flattenDocumentSymbols(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort((symbolA, symbolB) => Range.compareRangesUsingStarts(symbolA.range, symbolB.range)); - } - - private flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (const entry of entries) { - bucket.push({ - kind: entry.kind, - tags: entry.tags, - name: entry.name, - detail: entry.detail, - containerName: entry.containerName || overrideContainerLabel, - range: entry.range, - selectionRange: entry.selectionRange, - children: undefined, // we flatten it... - }); - - // Recurse over children - if (entry.children) { - this.flattenDocumentSymbols(bucket, entry.children, entry.name); - } - } + return token.isCancellationRequested ? [] : model.asListOfDocumentSymbols(); } } diff --git a/src/vs/editor/contrib/smartSelect/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/bracketSelections.ts index e5b8a52684..0f55b3fb4d 100644 --- a/src/vs/editor/contrib/smartSelect/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/bracketSelections.ts @@ -26,7 +26,7 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { return result; } - private static readonly _maxDuration = 30; + public static _maxDuration = 30; private static readonly _maxRounds = 2; private static _bracketsRightYield(resolve: () => void, round: number, model: ITextModel, pos: Position, ranges: Map>): void { diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 88a808dbf9..aaeb02b837 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -16,7 +16,7 @@ import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bra import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelect'; import { CancellationToken } from 'vs/base/common/cancellation'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/modelService.test'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; @@ -45,6 +45,16 @@ class MockJSMode extends MockMode { suite('SmartSelect', () => { + const OriginalBracketSelectionRangeProviderMaxDuration = BracketSelectionRangeProvider._maxDuration; + + suiteSetup(() => { + BracketSelectionRangeProvider._maxDuration = 5000; // 5 seconds + }); + + suiteTeardown(() => { + BracketSelectionRangeProvider._maxDuration = OriginalBracketSelectionRangeProviderMaxDuration; + }); + let modelService: ModelServiceImpl; let mode: MockJSMode; diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index 5c8b860805..a40ebb31c3 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -12,11 +12,11 @@ import { VariableResolver, Variable, Text } from 'vs/editor/contrib/snippet/snip import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { getLeadingWhitespace, commonPrefixLength, isFalsyOrWhitespace, splitLines } from 'vs/base/common/strings'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { toWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILabelService } from 'vs/platform/label/common/label'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { URI } from 'vs/base/common/uri'; import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; +import { generateUuid } from 'vs/base/common/uuid'; export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze({ 'CURRENT_YEAR': true, @@ -42,6 +42,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'TM_FILENAME_BASE': true, 'TM_DIRECTORY': true, 'TM_FILEPATH': true, + 'RELATIVE_FILEPATH': true, 'BLOCK_COMMENT_START': true, 'BLOCK_COMMENT_END': true, 'LINE_COMMENT': true, @@ -49,6 +50,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze( 'WORKSPACE_FOLDER': true, 'RANDOM': true, 'RANDOM_HEX': true, + 'UUID': true }); export class CompositeSnippetVariableResolver implements VariableResolver { @@ -177,6 +179,8 @@ export class ModelBasedVariableResolver implements VariableResolver { } else if (name === 'TM_FILEPATH' && this._labelService) { return this._labelService.getUriLabel(this._model.uri); + } else if (name === 'RELATIVE_FILEPATH' && this._labelService) { + return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true }); } return undefined; @@ -309,9 +313,9 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { return undefined; } - private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | URI): string | undefined { + private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return path.basename(workspaceIdentifier.path); + return path.basename(workspaceIdentifier.uri.path); } let filename = path.basename(workspaceIdentifier.configPath.path); @@ -320,9 +324,9 @@ export class WorkspaceBasedVariableResolver implements VariableResolver { } return filename; } - private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | URI): string | undefined { + private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { - return normalizeDriveLetter(workspaceIdentifier.fsPath); + return normalizeDriveLetter(workspaceIdentifier.uri.fsPath); } let filename = path.basename(workspaceIdentifier.configPath.path); @@ -340,9 +344,10 @@ export class RandomBasedVariableResolver implements VariableResolver { if (name === 'RANDOM') { return Math.random().toString().slice(-6); - } - else if (name === 'RANDOM_HEX') { + } else if (name === 'RANDOM_HEX') { return Math.random().toString(16).slice(-6); + } else if (name === 'UUID') { + return generateUuid(); } return undefined; diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts index aedf4866b0..eaac273466 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts @@ -62,34 +62,34 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); editor.trigger('test', 'type', { text: 'i' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var i; i < array.length; i++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[i];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var i; i < array.length; i++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[i];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.next(); editor.trigger('test', 'type', { text: 'arr' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var i; i < arr.length; i++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[i];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var i; i < arr.length; i++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[i];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.prev(); editor.trigger('test', 'type', { text: 'j' }); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var j; j < arr.length; j++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[j];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var j; j < arr.length; j++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = arr[j];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.next(); snippetController.next(); - assert.deepEqual(editor.getPosition(), new Position(6, 3)); + assert.deepStrictEqual(editor.getPosition(), new Position(6, 3)); }); }); @@ -98,13 +98,13 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); - assert.equal(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); - assert.equal(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); - assert.equal(editor.getModel()!.getLineContent(6), '\t\t'); - assert.equal(editor.getModel()!.getLineContent(7), '\t}'); + assert.strictEqual(editor.getModel()!.getLineContent(4), '\tfor (var index; index < array.length; index++) {'); + assert.strictEqual(editor.getModel()!.getLineContent(5), '\t\tvar element = array[index];'); + assert.strictEqual(editor.getModel()!.getLineContent(6), '\t\t'); + assert.strictEqual(editor.getModel()!.getLineContent(7), '\t}'); snippetController.cancel(); - assert.deepEqual(editor.getPosition(), new Position(4, 16)); + assert.deepStrictEqual(editor.getPosition(), new Position(4, 16)); }); }); @@ -121,7 +121,7 @@ suite('SnippetController', () => { // text: null // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -138,7 +138,7 @@ suite('SnippetController', () => { // text: null // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -155,7 +155,7 @@ suite('SnippetController', () => { // text: '\nHello' // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -172,7 +172,7 @@ suite('SnippetController', () => { // text: '\nHello' // }]); - // assert.equal(snippetController.isInSnippetMode(), false); + // assert.strictEqual(snippetController.isInSnippetMode(), false); // }); // }); @@ -183,7 +183,7 @@ suite('SnippetController', () => { editor.getModel()!.setValue('goodbye'); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -194,7 +194,7 @@ suite('SnippetController', () => { editor.getModel()!.undo(); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -205,7 +205,7 @@ suite('SnippetController', () => { editor.setPosition({ lineNumber: 1, column: 1 }); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -216,7 +216,7 @@ suite('SnippetController', () => { editor.setModel(null); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -227,7 +227,7 @@ suite('SnippetController', () => { snippetController.dispose(); - assert.equal(snippetController.isInSnippetMode(), false); + assert.strictEqual(snippetController.isInSnippetMode(), false); }); }); @@ -241,7 +241,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); @@ -256,7 +256,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0bar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); @@ -271,7 +271,7 @@ suite('SnippetController', () => { codeSnippet = 'foo$0bar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 14 }), second.toString()); @@ -286,7 +286,7 @@ suite('SnippetController', () => { codeSnippet = 'foo\n$0\nbar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); @@ -301,7 +301,7 @@ suite('SnippetController', () => { codeSnippet = 'foo\n$0\nbar'; snippetController.insert(codeSnippet); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString()); assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); @@ -315,7 +315,7 @@ suite('SnippetController', () => { codeSnippet = 'xo$0r'; snippetController.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 8, endColumn: 8, endLineNumber: 2 })); }); }); @@ -328,9 +328,9 @@ suite('SnippetController', () => { codeSnippet = '{{% url_**$1** %}}'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 1, startColumn: 27, endLineNumber: 1, endColumn: 27 })); - assert.equal(editor.getModel()!.getValue(), 'example example {{% url_**** %}}'); + assert.strictEqual(editor.getModel()!.getValue(), 'example example {{% url_**** %}}'); }, ['example example sc']); @@ -346,9 +346,9 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 2 }), editor.getSelection()!.toString()); - assert.equal(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); + assert.strictEqual(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); }, ['af']); @@ -364,9 +364,9 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), editor.getSelection()!.toString()); - assert.equal(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); + assert.strictEqual(editor.getModel()!.getValue(), 'afterEach((done) => {\n\ttest\n});'); }, ['af']); @@ -380,8 +380,8 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 8 }); - assert.equal(editor.getModel()!.getValue(), 'after'); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getModel()!.getValue(), 'after'); + assert.strictEqual(editor.getSelections()!.length, 1); assert.ok(editor.getSelection()!.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), editor.getSelection()!.toString()); }, ['afterone']); @@ -404,7 +404,7 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 2); + assert.strictEqual(editor.getSelections()!.length, 2); const [first, second] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 5, startColumn: 3, endLineNumber: 5, endColumn: 3 }), first.toString()); @@ -429,7 +429,7 @@ suite('SnippetController', () => { controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getSelections()!.length, 1); + assert.strictEqual(editor.getSelections()!.length, 1); const [first] = editor.getSelections()!; assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 3 }), first.toString()); @@ -465,7 +465,7 @@ suite('SnippetController', () => { codeSnippet = '_foo'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc_foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc_foo'); }, ['this._', 'abc']); @@ -478,7 +478,7 @@ suite('SnippetController', () => { codeSnippet = 'XX'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this.XX\nabcXX'); + assert.strictEqual(editor.getModel()!.getValue(), 'this.XX\nabcXX'); }, ['this._', 'abc']); @@ -492,7 +492,7 @@ suite('SnippetController', () => { codeSnippet = '_foo'; controller.insert(codeSnippet, { overwriteBefore: 1 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc_foo\ndef_foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc_foo\ndef_foo'); }, ['this._', 'abc', 'def_']); @@ -506,7 +506,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); }, ['this._', 'abc', 'def._']); @@ -520,7 +520,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._foo\nabc._foo\ndef._foo'); }, ['this._', 'abc', 'def._']); @@ -534,7 +534,7 @@ suite('SnippetController', () => { codeSnippet = '._foo'; controller.insert(codeSnippet, { overwriteBefore: 2 }); - assert.equal(editor.getModel()!.getValue(), 'this._._foo\na._foo\ndef._._foo'); + assert.strictEqual(editor.getModel()!.getValue(), 'this._._foo\na._foo\ndef._._foo'); }, ['this._', 'abc', 'def._']); @@ -550,7 +550,7 @@ suite('SnippetController', () => { codeSnippet = 'document'; controller.insert(codeSnippet, { overwriteBefore: 3 }); - assert.equal(editor.getModel()!.getValue(), '{document}\n{document && true}'); + assert.strictEqual(editor.getModel()!.getValue(), '{document}\n{document && true}'); }, ['{foo}', '{foo && true}']); }); @@ -565,7 +565,7 @@ suite('SnippetController', () => { codeSnippet = 'for (var ${1:i}=0; ${1:i} { codeSnippet = 'for (let ${1:i}=0; ${1:i} expected=${actual.toString()}`); } - assert.equal(s.length, 0); + assert.strictEqual(s.length, 0); } function assertContextKeys(service: MockContextKeyService, inSnippet: boolean, hasPrev: boolean, hasNext: boolean): void { - assert.equal(SnippetController2.InSnippetMode.getValue(service), inSnippet, `inSnippetMode`); - assert.equal(SnippetController2.HasPrevTabstop.getValue(service), hasPrev, `HasPrevTabstop`); - assert.equal(SnippetController2.HasNextTabstop.getValue(service), hasNext, `HasNextTabstop`); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(service), inSnippet, `inSnippetMode`); + assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(service), hasPrev, `HasPrevTabstop`); + assert.strictEqual(SnippetController2.HasNextTabstop.getValue(service), hasNext, `HasNextTabstop`); } let editor: ICodeEditor; @@ -40,7 +40,7 @@ suite('SnippetController2', function () { model = createTextModel('if\n $state\nfi'); editor = createTestCodeEditor({ model: model }); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); }); teardown(function () { @@ -78,9 +78,9 @@ suite('SnippetController2', function () { assertContextKeys(contextKeys, false, false, false); editor.trigger('test', 'type', { text: '\t' }); - assert.equal(SnippetController2.InSnippetMode.getValue(contextKeys), false); - assert.equal(SnippetController2.HasNextTabstop.getValue(contextKeys), false); - assert.equal(SnippetController2.HasPrevTabstop.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.HasNextTabstop.getValue(contextKeys), false); + assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(contextKeys), false); }); test('insert, insert -> cursor moves out (left/right)', function () { @@ -111,7 +111,7 @@ suite('SnippetController2', function () { const ctrl = new SnippetController2(editor, logService, contextKeys); ctrl.insert('foo${1:bar}foo$0'); - assert.equal(SnippetController2.InSnippetMode.getValue(contextKeys), true); + assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), true); assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); // bad selection change diff --git a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts index a07cdf7f5a..4af65bdffc 100644 --- a/src/vs/editor/contrib/snippet/test/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetParser.test.ts @@ -531,8 +531,8 @@ suite('SnippetParser', () => { let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true); let [first, second] = snippet.placeholders; - assert.deepEqual(snippet.enclosingPlaceholders(first), []); - assert.deepEqual(snippet.enclosingPlaceholders(second), [first]); + assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []); + assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]); }); test('TextmateSnippet#offset', () => { diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index f455d37a5a..4e42b3bbc5 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -23,14 +23,14 @@ suite('SnippetSession', function () { const actual = s.shift()!; assert.ok(selection.equalsSelection(actual), `actual=${selection.toString()} <> expected=${actual.toString()}`); } - assert.equal(s.length, 0); + assert.strictEqual(s.length, 0); } setup(function () { model = createTextModel('function foo() {\n console.log(a);\n}'); editor = createTestCodeEditor({ model: model }) as IActiveCodeEditor; editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); }); teardown(function () { @@ -43,7 +43,7 @@ suite('SnippetSession', function () { function assertNormalized(position: IPosition, input: string, expected: string): void { const snippet = new SnippetParser().parse(input); SnippetSession.adjustWhitespace(model, position, snippet, true, true); - assert.equal(snippet.toTextmateString(), expected); + assert.strictEqual(snippet.toTextmateString(), expected); } assertNormalized(new Position(1, 1), 'foo', 'foo'); @@ -73,7 +73,7 @@ suite('SnippetSession', function () { test('text edits & selection', function () { const session = new SnippetSession(editor, 'foo${1:bar}foo$0'); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); session.next(); @@ -86,7 +86,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(2, 5, 2, 5), new Selection(1, 1, 1, 1)]); session.insert(); - assert.equal(model.getValue(), 'barfunction foo() {\n barconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'barfunction foo() {\n barconsole.log(a);\n}'); assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(1, 1, 1, 4)); }); @@ -107,7 +107,7 @@ suite('SnippetSession', function () { test('snippets, just text', function () { const session = new SnippetSession(editor, 'foobar'); session.insert(); - assert.equal(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'foobarfunction foo() {\n foobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); }); @@ -116,7 +116,7 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0'); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\n console.log(a);\n}'); assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(5, 9, 5, 12)); @@ -129,7 +129,7 @@ suite('SnippetSession', function () { editor.setSelection(new Selection(2, 5, 2, 5)); const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }); session.insert(); - assert.equal(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); + assert.strictEqual(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); }); test('snippets, selections -> next/prev', () => { @@ -171,7 +171,7 @@ suite('SnippetSession', function () { // go to final tabstop session.next(); - assert.equal(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}'); + assert.strictEqual(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}'); assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12)); }); @@ -180,7 +180,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'x$0').insert(); - assert.equal(model.getValue(), 'x_bar_x'); + assert.strictEqual(model.getValue(), 'x_bar_x'); assertSelections(editor, new Selection(1, 2, 1, 2), new Selection(1, 8, 1, 8)); }); @@ -189,7 +189,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'LONGER$0').insert(); - assert.equal(model.getValue(), 'LONGER_bar_LONGER'); + assert.strictEqual(model.getValue(), 'LONGER_bar_LONGER'); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(1, 18, 1, 18)); }); @@ -203,11 +203,11 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo-' }); session.next(); - assert.equal(model.getValue(), 'foo_foo-bar_foo'); + assert.strictEqual(model.getValue(), 'foo_foo-bar_foo'); assertSelections(editor, new Selection(1, 12, 1, 12)); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getValue(), 'foo_foo-barXXX_foo'); + assert.strictEqual(model.getValue(), 'foo_foo-barXXX_foo'); session.prev(); assertSelections(editor, new Selection(1, 5, 1, 9)); session.next(); @@ -242,7 +242,7 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '333' }); session.next(); - assert.equal(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); + assert.strictEqual(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); session.prev(); @@ -269,22 +269,22 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '333' }); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); }); test('snippets, gracefully move over final tabstop', function () { const session = new SnippetSession(editor, '${1}bar$0'); session.insert(); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); }); @@ -294,46 +294,46 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 5, 1, 7), new Selection(2, 9, 2, 11)); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getValue(), 'log(XXX);function foo() {\n log(XXX);console.log(a);\n}'); + assert.strictEqual(model.getValue(), 'log(XXX);function foo() {\n log(XXX);console.log(a);\n}'); session.next(); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); // assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); }); test('snippets, selections and snippet ranges', function () { const session = new SnippetSession(editor, '${1:foo}farboo${2:bar}$0'); session.insert(); - assert.equal(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); + assert.strictEqual(model.getValue(), 'foofarboobarfunction foo() {\n foofarboobarconsole.log(a);\n}'); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); - assert.equal(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); editor.setSelections([new Selection(1, 1, 1, 1)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10), new Selection(1, 1, 1, 1)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // in snippet, outside placeholder editor.setSelections([new Selection(1, 6, 1, 6), new Selection(2, 10, 2, 10), new Selection(2, 20, 2, 21)]); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); assertSelections(editor, new Selection(1, 10, 1, 13), new Selection(2, 14, 2, 17)); // reset selection to placeholder session.next(); - assert.equal(session.isSelectionWithinPlaceholders(), true); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13), new Selection(2, 17, 2, 17)); }); @@ -344,20 +344,20 @@ suite('SnippetSession', function () { const first = new SnippetSession(editor, 'foo${2:bar}foo$0'); first.insert(); - assert.equal(model.getValue(), 'foobarfoo'); + assert.strictEqual(model.getValue(), 'foobarfoo'); assertSelections(editor, new Selection(1, 4, 1, 7)); const second = new SnippetSession(editor, 'ba${1:zzzz}$0'); second.insert(); - assert.equal(model.getValue(), 'foobazzzzfoo'); + assert.strictEqual(model.getValue(), 'foobazzzzfoo'); assertSelections(editor, new Selection(1, 6, 1, 10)); second.next(); - assert.equal(second.isAtLastPlaceholder, true); + assert.strictEqual(second.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 10, 1, 10)); first.next(); - assert.equal(first.isAtLastPlaceholder, true); + assert.strictEqual(first.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 13, 1, 13)); }); @@ -365,11 +365,11 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, 'farboo$0'); session.insert(); - assert.equal(session.isAtLastPlaceholder, true); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); }); test('snippets, typing at beginning', function () { @@ -379,12 +379,12 @@ suite('SnippetSession', function () { session.insert(); editor.setSelection(new Selection(1, 2, 1, 2)); - assert.equal(session.isSelectionWithinPlaceholders(), false); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(session.isAtLastPlaceholder, true); editor.trigger('test', 'type', { text: 'XXX' }); - assert.equal(model.getLineContent(1), 'fXXXfarboounction foo() {'); - assert.equal(session.isSelectionWithinPlaceholders(), false); + assert.strictEqual(model.getLineContent(1), 'fXXXfarboounction foo() {'); + assert.strictEqual(session.isSelectionWithinPlaceholders(), false); session.next(); assertSelections(editor, new Selection(1, 11, 1, 11)); @@ -412,7 +412,7 @@ suite('SnippetSession', function () { const session = new SnippetSession(editor, '@line=$TM_LINE_NUMBER$0'); session.insert(); - assert.equal(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); + assert.strictEqual(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12)); }); @@ -428,10 +428,10 @@ suite('SnippetSession', function () { session.next(); assertSelections(editor, new Selection(1, 22, 1, 22)); - assert.equal(session.isAtLastPlaceholder, false); + assert.strictEqual(session.isAtLastPlaceholder, false); session.next(); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 23, 1, 23)); session.prev(); @@ -456,8 +456,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo' }); session.next(); - assert.equal(model.getValue(), 'bar'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'bar'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 4, 1, 4)); }); @@ -471,8 +471,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'foo' }); session.next(); - assert.equal(model.getValue(), 'foo baz bar'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'foo baz bar'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 12, 1, 12)); }); @@ -493,8 +493,8 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 16, 1, 16)); session.next(); - assert.equal(model.getValue(), 'clk : std_logic;\n'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'clk : std_logic;\n'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(2, 1, 2, 1)); }); @@ -532,8 +532,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: 'string' }); session.next(); - assert.equal(model.getValue(), expected); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), expected); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(4, 2, 4, 2)); }); @@ -556,8 +556,8 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: ' := \'1\'' }); session.next(); - assert.equal(model.getValue(), 'clk : std_logic := \'1\';\n'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), 'clk : std_logic := \'1\';\n'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(2, 1, 2, 1)); }); @@ -570,13 +570,13 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); session.next(); - assert.equal(model.getValue(), '{fff}'); + assert.strictEqual(model.getValue(), '{fff}'); assertSelections(editor, new Selection(1, 2, 1, 5)); editor.trigger('test', 'type', { text: 'ggg' }); session.next(); - assert.equal(model.getValue(), '{ggg}'); - assert.equal(session.isAtLastPlaceholder, true); + assert.strictEqual(model.getValue(), '{ggg}'); + assert.strictEqual(session.isAtLastPlaceholder, true); assertSelections(editor, new Selection(1, 6, 1, 6)); }); @@ -584,7 +584,7 @@ suite('SnippetSession', function () { editor.getModel().setValue(''); const session = new SnippetSession(editor, '${1:{}${2:fff}${1/[\\{]/}/}$0'); session.insert(); - assert.equal(editor.getModel().getValue(), '{fff{'); + assert.strictEqual(editor.getModel().getValue(), '{fff{'); assertSelections(editor, new Selection(1, 1, 1, 2), new Selection(1, 5, 1, 6)); session.next(); @@ -599,25 +599,25 @@ suite('SnippetSession', function () { editor.trigger('test', 'type', { text: '1' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '2' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '3' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\n'); session.merge('test ${1:replaceme}'); editor.trigger('test', 'type', { text: '4' }); editor.trigger('test', 'type', { text: '\n' }); - assert.equal(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\ntest 4\n'); + assert.strictEqual(editor.getModel()!.getValue(), 'test 1\ntest 2\ntest 3\ntest 4\n'); }); test('Snippet variable text isn\'t whitespace normalised, #31124', function () { @@ -642,7 +642,7 @@ suite('SnippetSession', function () { 'end' ].join('\n'); - assert.equal(editor.getModel()!.getValue(), expected); + assert.strictEqual(editor.getModel()!.getValue(), expected); editor.getModel()!.setValue([ 'start', @@ -665,7 +665,7 @@ suite('SnippetSession', function () { 'end' ].join('\n'); - assert.equal(editor.getModel()!.getValue(), expected); + assert.strictEqual(editor.getModel()!.getValue(), expected); }); test('Selecting text from left to right, and choosing item messes up code, #31199', function () { @@ -680,7 +680,7 @@ suite('SnippetSession', function () { editor.setSelections([new Selection(1, 9, 1, 12)]); new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }).insert(); - assert.equal(model.getValue(), 'console.far'); + assert.strictEqual(model.getValue(), 'console.far'); }); test('Tabs don\'t get replaced with spaces in snippet transformations #103818', function () { diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 2624580081..ffb11af720 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -9,11 +9,14 @@ import { Selection } from 'vs/editor/common/core/selection'; import { SelectionBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, ClipboardBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from 'vs/editor/contrib/snippet/snippetVariables'; import { SnippetParser, Variable, VariableResolver } from 'vs/editor/contrib/snippet/snippetParser'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { toWorkspaceFolders, IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { mock } from 'vs/base/test/common/mock'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { sep } from 'vs/base/common/path'; +import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; suite('Snippet Variables Resolver', function () { @@ -48,9 +51,9 @@ suite('Snippet Variables Resolver', function () { const variable = snippet.children[0]; variable.resolve(resolver); if (variable.children.length === 0) { - assert.equal(undefined, expected); + assert.strictEqual(undefined, expected); } else { - assert.equal(variable.toString(), expected); + assert.strictEqual(variable.toString(), expected); } } @@ -127,17 +130,17 @@ suite('Snippet Variables Resolver', function () { test('TextmateSnippet, resolve variable', function () { const snippet = new SnippetParser().parse('"$TM_CURRENT_WORD"', true); - assert.equal(snippet.toString(), '""'); + assert.strictEqual(snippet.toString(), '""'); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), '"this"'); + assert.strictEqual(snippet.toString(), '"this"'); }); test('TextmateSnippet, resolve variable with default', function () { const snippet = new SnippetParser().parse('"${TM_CURRENT_WORD:foo}"', true); - assert.equal(snippet.toString(), '"foo"'); + assert.strictEqual(snippet.toString(), '"foo"'); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), '"this"'); + assert.strictEqual(snippet.toString(), '"this"'); }); test('More useful environment variables for snippets, #32737', function () { @@ -169,14 +172,14 @@ suite('Snippet Variables Resolver', function () { .resolveVariables({ resolve(variable) { return varValue || variable.name; } }); const actual = snippet.toString(); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } test('Variable Snippet Transform', function () { const snippet = new SnippetParser().parse('name=${TM_FILENAME/(.*)\\..+$/$1/}', true); snippet.resolveVariables(resolver); - assert.equal(snippet.toString(), 'name=text'); + assert.strictEqual(snippet.toString(), 'name=text'); assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2/}', 'Var'); assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2-${1:/downcase}/}', 'Var-t'); @@ -267,7 +270,7 @@ suite('Snippet Variables Resolver', function () { const snippet = new SnippetParser().parse(`$${varName}`); const variable = snippet.children[0]; - assert.equal(variable.resolve(resolver), true, `${varName} failed to resolve`); + assert.strictEqual(variable.resolve(resolver), true, `${varName} failed to resolve`); } test('Add time variables for snippets #41631, #43140', function () { @@ -292,10 +295,10 @@ suite('Snippet Variables Resolver', function () { const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true); snippet.resolveVariables({ resolve() { return '10'; } }); - assert.equal(snippet.toString(), 'It is line 10'); + assert.strictEqual(snippet.toString(), 'It is line 10'); snippet.resolveVariables({ resolve() { return '11'; } }); - assert.equal(snippet.toString(), 'It is not line 10'); + assert.strictEqual(snippet.toString(), 'It is not line 10'); }); test('Add workspace name and folder variables for snippets #68261', function () { @@ -332,10 +335,55 @@ suite('Snippet Variables Resolver', function () { // workspace with config const workspaceConfigPath = URI.file('testWorkspace.code-workspace'); - workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath), workspaceConfigPath); + workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath); assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace'); if (!isWindows) { assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/'); } }); + + test('Add RELATIVE_FILEPATH snippet variable #114208', function () { + + let resolver: VariableResolver; + + // Mock a label service (only coded for file uris) + const workspaceLabelService = ((rootPath: string): ILabelService => { + const labelService = new class extends mock() { + getUriLabel(uri: URI, options: { relative?: boolean } = {}) { + const rootFsPath = URI.file(rootPath).fsPath + sep; + const fsPath = uri.fsPath; + if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) { + return fsPath.substring(rootFsPath.length); + } + return fsPath; + } + }; + return labelService; + }); + + const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt')); + + // empty workspace + resolver = new ModelBasedVariableResolver( + workspaceLabelService(''), + model + ); + + if (!isWindows) { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt'); + } else { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt'); + } + + // single folder workspace + resolver = new ModelBasedVariableResolver( + workspaceLabelService('/foo'), + model + ); + if (!isWindows) { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt'); + } else { + assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt'); + } + }); }); diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index ffa4ea3b37..57f62d1499 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -443,10 +443,6 @@ export class SuggestModel implements IDisposable { this._requestToken?.dispose(); - if (this._state === State.Idle) { - return; - } - if (!this._editor.hasModel()) { return; } @@ -456,6 +452,10 @@ export class SuggestModel implements IDisposable { clipboardText = await this._clipboardService.readText(); } + if (this._state === State.Idle) { + return; + } + const model = this._editor.getModel(); let items = completions.items; diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 6e2064dd89..ce6316a5d9 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/suggest'; import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded -import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded +import 'vs/editor/contrib/symbolIcons/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; @@ -103,7 +103,7 @@ export class SuggestWidget implements IDisposable { private _focusedItem?: CompletionItem; private _ignoreFocusEvents: boolean = false; private _completionModel?: CompletionModel; - private _cappedHeight?: { wanted: number, capped: number }; + private _cappedHeight?: { wanted: number; capped: number; }; private _explainMode: boolean = false; readonly element: ResizableHTMLElement; @@ -217,6 +217,7 @@ export class SuggestWidget implements IDisposable { getHeight: (_element: CompletionItem): number => this.getLayoutInfo().itemHeight, getTemplateId: (_element: CompletionItem): string => 'suggestion' }, [renderer], { + alwaysConsumeMouseWheel: true, useShadows: false, mouseSupport: false, accessibilityProvider: { @@ -436,6 +437,7 @@ export class SuggestWidget implements IDisposable { case State.Hidden: dom.hide(this._messageElement, this._listElement, this._status.element); this._details.hide(true); + this._status.hide(); this._contentWidget.hide(); this._ctxSuggestWidgetVisible.reset(); this._ctxSuggestWidgetMultipleSuggestions.reset(); @@ -483,6 +485,7 @@ export class SuggestWidget implements IDisposable { } private _show(): void { + this._status.show(); this._contentWidget.show(); this._layout(this._persistedSize.restore()); this._ctxSuggestWidgetVisible.set(true); @@ -702,6 +705,14 @@ export class SuggestWidget implements IDisposable { this._loadingTimeout?.dispose(); this._setState(State.Hidden); this._onDidHide.fire(this); + + // ensure that a reasonable widget height is persisted so that + // accidential "resize-to-single-items" cases aren't happening + const dim = this._persistedSize.restore(); + const minPersistedHeight = Math.ceil(this.getLayoutInfo().itemHeight * 4.3); + if (dim && dim.height < minPersistedHeight) { + this._persistedSize.store(dim.with(undefined, minPersistedHeight)); + } } isFrozen(): boolean { @@ -734,12 +745,16 @@ export class SuggestWidget implements IDisposable { return; } - let height = size?.height; - let width = size?.width; - const bodyBox = dom.getClientArea(document.body); const info = this.getLayoutInfo(); + if (!size) { + size = info.defaultSize; + } + + let height = size.height; + let width = size.width; + // status bar this._status.element.style.lineHeight = `${info.itemHeight}px`; @@ -756,9 +771,6 @@ export class SuggestWidget implements IDisposable { // width math const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding; - if (width === undefined) { - width = info.defaultSize.width; - } if (width > maxWidth) { width = maxWidth; } @@ -766,7 +778,6 @@ export class SuggestWidget implements IDisposable { // height math const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight; - const preferredHeight = info.defaultSize.height; const minHeight = info.itemHeight + info.statusBarHeight; const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode()); const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition()); @@ -775,15 +786,12 @@ export class SuggestWidget implements IDisposable { const maxHeightAbove = Math.min(editorBox.top + cursorBox.top - info.verticalPadding, fullHeight); let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight); - if (height && height === this._cappedHeight?.capped) { + if (height === this._cappedHeight?.capped) { // Restore the old (wanted) height when the current // height is capped to fit height = this._cappedHeight.wanted; } - if (height === undefined) { - height = Math.min(preferredHeight, fullHeight); - } if (height < minHeight) { height = minHeight; } @@ -801,14 +809,14 @@ export class SuggestWidget implements IDisposable { this.element.enableSashes(false, true, true, false); maxHeight = maxHeightBelow; } - this.element.preferredSize = new dom.Dimension(preferredWidth, preferredHeight); + this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height); this.element.maxSize = new dom.Dimension(maxWidth, maxHeight); this.element.minSize = new dom.Dimension(220, minHeight); // Know when the height was capped to fit and remember // the wanted height for later. This is required when going // left to widen suggestions. - this._cappedHeight = size && height === fullHeight + this._cappedHeight = height === fullHeight ? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height } : undefined; } diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index 2818ced5e8..480c5b6c09 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -33,7 +33,7 @@ export const suggestMoreInfoIcon = registerIcon('suggest-more-info', Codicon.che const _completionItemColor = new class ColorExtractor { - private static _regexRelaxed = /(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))/; + private static _regexRelaxed = /(#([\da-fA-F]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))/; private static _regexStrict = new RegExp(`^${ColorExtractor._regexRelaxed.source}$`, 'i'); extract(item: CompletionItem, out: string[]): boolean { @@ -117,7 +117,7 @@ export class ItemRenderer implements IListRenderer(action => { - return action instanceof MenuItemAction - ? instantiationService.createInstance(StatusBarViewItem, action) - : undefined; + return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action) : undefined; }); - const leftActions = new ActionBar(this.element, { actionViewItemProvider }); - const rightActions = new ActionBar(this.element, { actionViewItemProvider }); - const menu = menuService.createMenu(suggestWidgetStatusbarMenu, contextKeyService); + this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); + this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); - leftActions.domNode.classList.add('left'); - rightActions.domNode.classList.add('right'); + this._leftActions.domNode.classList.add('left'); + this._rightActions.domNode.classList.add('right'); + } + dispose(): void { + this._menuDisposables.dispose(); + this.element.remove(); + } + + show(): void { + const menu = this._menuService.createMenu(suggestWidgetStatusbarMenu, this._contextKeyService); const renderMenu = () => { const left: IAction[] = []; const right: IAction[] = []; @@ -68,17 +75,16 @@ export class SuggestWidgetStatus { right.push(...actions); } } - leftActions.clear(); - leftActions.push(left); - rightActions.clear(); - rightActions.push(right); + this._leftActions.clear(); + this._leftActions.push(left); + this._rightActions.clear(); + this._rightActions.push(right); }; - this._disposables.add(menu.onDidChange(() => renderMenu())); - this._disposables.add(menu); + this._menuDisposables.add(menu.onDidChange(() => renderMenu())); + this._menuDisposables.add(menu); } - dispose(): void { - this._disposables.dispose(); - this.element.remove(); + hide(): void { + this._menuDisposables.clear(); } } diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index 27202cc34a..732fd5564a 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -57,6 +57,7 @@ suite('SuggestController', function () { createMenu() { return new class extends mock() { onDidChange = Event.None; + dispose() { } }; } }] diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 2b0e577ed6..14d1565dd8 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -71,7 +71,7 @@ suite('SuggestModel - Context', function () { this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; let prevLanguageId: LanguageIdentifier | undefined = undefined; for (let i = 0; i < line.length; i++) { diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/symbolIcons/symbolIcons.ts similarity index 57% rename from src/vs/editor/contrib/documentSymbols/outlineTree.ts rename to src/vs/editor/contrib/symbolIcons/symbolIcons.ts index dbeb8a6161..9f9b74f947 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/symbolIcons/symbolIcons.ts @@ -3,359 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from 'vs/base/browser/dom'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IDataSource, ITreeNode, ITreeRenderer, ITreeSorter, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; -import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import 'vs/css!./media/outlineTree'; -import 'vs/css!./media/symbol-icons'; -import { Range } from 'vs/editor/common/core/range'; -import { SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { OutlineConfigKeys } from 'vs/editor/contrib/documentSymbols/outline'; -import { MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { registerColor, listErrorForeground, listWarningForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { IdleValue } from 'vs/base/common/async'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { URI } from 'vs/base/common/uri'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { Iterable } from 'vs/base/common/iterator'; +import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { registerColor, foreground } from 'vs/platform/theme/common/colorRegistry'; import { Codicon } from 'vs/base/common/codicons'; -export type OutlineItem = OutlineGroup | OutlineElement; - -export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelProvider { - - getKeyboardNavigationLabel(element: OutlineItem): { toString(): string; } { - if (element instanceof OutlineGroup) { - return element.label; - } else { - return element.symbol.name; - } - } -} - -export class OutlineAccessibilityProvider implements IListAccessibilityProvider { - - constructor(private readonly ariaLabel: string) { } - - getWidgetAriaLabel(): string { - return this.ariaLabel; - } - - getAriaLabel(element: OutlineItem): string | null { - if (element instanceof OutlineGroup) { - return element.label; - } else { - return element.symbol.name; - } - } -} - -export class OutlineIdentityProvider implements IIdentityProvider { - getId(element: OutlineItem): { toString(): string; } { - return element.id; - } -} - -export class OutlineGroupTemplate { - static readonly id = 'OutlineGroupTemplate'; - constructor( - readonly labelContainer: HTMLElement, - readonly label: HighlightedLabel, - ) { } -} - -export class OutlineElementTemplate { - static readonly id = 'OutlineElementTemplate'; - constructor( - readonly container: HTMLElement, - readonly iconLabel: IconLabel, - readonly iconClass: HTMLElement, - readonly decoration: HTMLElement, - ) { } -} - -export class OutlineVirtualDelegate implements IListVirtualDelegate { - - getHeight(_element: OutlineItem): number { - return 22; - } - - getTemplateId(element: OutlineItem): string { - if (element instanceof OutlineGroup) { - return OutlineGroupTemplate.id; - } else { - return OutlineElementTemplate.id; - } - } -} - -export class OutlineGroupRenderer implements ITreeRenderer { - - readonly templateId: string = OutlineGroupTemplate.id; - - renderTemplate(container: HTMLElement): OutlineGroupTemplate { - const labelContainer = dom.$('.outline-element-label'); - container.classList.add('outline-element'); - dom.append(container, labelContainer); - return new OutlineGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true)); - } - - renderElement(node: ITreeNode, index: number, template: OutlineGroupTemplate): void { - template.label.set( - node.element.label, - createMatches(node.filterData) - ); - } - - disposeTemplate(_template: OutlineGroupTemplate): void { - // nothing - } -} - -export class OutlineElementRenderer implements ITreeRenderer { - - readonly templateId: string = OutlineElementTemplate.id; - - constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IThemeService private readonly _themeService: IThemeService, - ) { } - - renderTemplate(container: HTMLElement): OutlineElementTemplate { - container.classList.add('outline-element'); - const iconLabel = new IconLabel(container, { supportHighlights: true }); - const iconClass = dom.$('.outline-element-icon'); - const decoration = dom.$('.outline-element-decoration'); - container.prepend(iconClass); - container.appendChild(decoration); - return new OutlineElementTemplate(container, iconLabel, iconClass, decoration); - } - - renderElement(node: ITreeNode, index: number, template: OutlineElementTemplate): void { - const { element } = node; - const options = { - matches: createMatches(node.filterData), - labelEscapeNewLines: true, - extraClasses: [], - title: localize('title.template', "{0} ({1})", element.symbol.name, OutlineElementRenderer._symbolKindNames[element.symbol.kind]) - }; - if (this._configurationService.getValue(OutlineConfigKeys.icons)) { - // add styles for the icons - template.iconClass.className = ''; - template.iconClass.classList.add(`outline-element-icon`, ...SymbolKinds.toCssClassName(element.symbol.kind, true).split(' ')); - } - if (element.symbol.tags.indexOf(SymbolTag.Deprecated) >= 0) { - options.extraClasses.push(`deprecated`); - options.matches = []; - } - template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options); - this._renderMarkerInfo(element, template); - } - - private _renderMarkerInfo(element: OutlineElement, template: OutlineElementTemplate): void { - - if (!element.marker) { - dom.hide(template.decoration); - template.container.style.removeProperty('--outline-element-color'); - return; - } - - const { count, topSev } = element.marker; - const color = this._themeService.getColorTheme().getColor(topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground); - const cssColor = color ? color.toString() : 'inherit'; - - // color of the label - if (this._configurationService.getValue(OutlineConfigKeys.problemsColors)) { - template.container.style.setProperty('--outline-element-color', cssColor); - } else { - template.container.style.removeProperty('--outline-element-color'); - } - - // badge with color/rollup - if (!this._configurationService.getValue(OutlineConfigKeys.problemsBadges)) { - dom.hide(template.decoration); - - } else if (count > 0) { - dom.show(template.decoration); - template.decoration.classList.remove('bubble'); - template.decoration.innerText = count < 10 ? count.toString() : '+9'; - template.decoration.title = count === 1 ? localize('1.problem', "1 problem in this element") : localize('N.problem', "{0} problems in this element", count); - template.decoration.style.setProperty('--outline-element-color', cssColor); - - } else { - dom.show(template.decoration); - template.decoration.classList.add('bubble'); - template.decoration.innerText = '\uea71'; - template.decoration.title = localize('deep.problem', "Contains elements with problems"); - template.decoration.style.setProperty('--outline-element-color', cssColor); - } - } - - private static _symbolKindNames: { [symbol: number]: string } = { - [SymbolKind.Array]: localize('Array', "array"), - [SymbolKind.Boolean]: localize('Boolean', "boolean"), - [SymbolKind.Class]: localize('Class', "class"), - [SymbolKind.Constant]: localize('Constant', "constant"), - [SymbolKind.Constructor]: localize('Constructor', "constructor"), - [SymbolKind.Enum]: localize('Enum', "enumeration"), - [SymbolKind.EnumMember]: localize('EnumMember', "enumeration member"), - [SymbolKind.Event]: localize('Event', "event"), - [SymbolKind.Field]: localize('Field', "field"), - [SymbolKind.File]: localize('File', "file"), - [SymbolKind.Function]: localize('Function', "function"), - [SymbolKind.Interface]: localize('Interface', "interface"), - [SymbolKind.Key]: localize('Key', "key"), - [SymbolKind.Method]: localize('Method', "method"), - [SymbolKind.Module]: localize('Module', "module"), - [SymbolKind.Namespace]: localize('Namespace', "namespace"), - [SymbolKind.Null]: localize('Null', "null"), - [SymbolKind.Number]: localize('Number', "number"), - [SymbolKind.Object]: localize('Object', "object"), - [SymbolKind.Operator]: localize('Operator', "operator"), - [SymbolKind.Package]: localize('Package', "package"), - [SymbolKind.Property]: localize('Property', "property"), - [SymbolKind.String]: localize('String', "string"), - [SymbolKind.Struct]: localize('Struct', "struct"), - [SymbolKind.TypeParameter]: localize('TypeParameter', "type parameter"), - [SymbolKind.Variable]: localize('Variable', "variable"), - }; - - disposeTemplate(_template: OutlineElementTemplate): void { - _template.iconLabel.dispose(); - } -} - -export const enum OutlineSortOrder { - ByPosition, - ByName, - ByKind -} - -export class OutlineFilter implements ITreeFilter { - - static readonly configNameToKind = Object.freeze({ - ['showFiles']: SymbolKind.File, - ['showModules']: SymbolKind.Module, - ['showNamespaces']: SymbolKind.Namespace, - ['showPackages']: SymbolKind.Package, - ['showClasses']: SymbolKind.Class, - ['showMethods']: SymbolKind.Method, - ['showProperties']: SymbolKind.Property, - ['showFields']: SymbolKind.Field, - ['showConstructors']: SymbolKind.Constructor, - ['showEnums']: SymbolKind.Enum, - ['showInterfaces']: SymbolKind.Interface, - ['showFunctions']: SymbolKind.Function, - ['showVariables']: SymbolKind.Variable, - ['showConstants']: SymbolKind.Constant, - ['showStrings']: SymbolKind.String, - ['showNumbers']: SymbolKind.Number, - ['showBooleans']: SymbolKind.Boolean, - ['showArrays']: SymbolKind.Array, - ['showObjects']: SymbolKind.Object, - ['showKeys']: SymbolKind.Key, - ['showNull']: SymbolKind.Null, - ['showEnumMembers']: SymbolKind.EnumMember, - ['showStructs']: SymbolKind.Struct, - ['showEvents']: SymbolKind.Event, - ['showOperators']: SymbolKind.Operator, - ['showTypeParameters']: SymbolKind.TypeParameter, - }); - - static readonly kindToConfigName = Object.freeze({ - [SymbolKind.File]: 'showFiles', - [SymbolKind.Module]: 'showModules', - [SymbolKind.Namespace]: 'showNamespaces', - [SymbolKind.Package]: 'showPackages', - [SymbolKind.Class]: 'showClasses', - [SymbolKind.Method]: 'showMethods', - [SymbolKind.Property]: 'showProperties', - [SymbolKind.Field]: 'showFields', - [SymbolKind.Constructor]: 'showConstructors', - [SymbolKind.Enum]: 'showEnums', - [SymbolKind.Interface]: 'showInterfaces', - [SymbolKind.Function]: 'showFunctions', - [SymbolKind.Variable]: 'showVariables', - [SymbolKind.Constant]: 'showConstants', - [SymbolKind.String]: 'showStrings', - [SymbolKind.Number]: 'showNumbers', - [SymbolKind.Boolean]: 'showBooleans', - [SymbolKind.Array]: 'showArrays', - [SymbolKind.Object]: 'showObjects', - [SymbolKind.Key]: 'showKeys', - [SymbolKind.Null]: 'showNull', - [SymbolKind.EnumMember]: 'showEnumMembers', - [SymbolKind.Struct]: 'showStructs', - [SymbolKind.Event]: 'showEvents', - [SymbolKind.Operator]: 'showOperators', - [SymbolKind.TypeParameter]: 'showTypeParameters', - }); - - constructor( - private readonly _prefix: string, - @ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService, - ) { } - - filter(element: OutlineItem): boolean { - const outline = OutlineModel.get(element); - let uri: URI | undefined; - - if (outline) { - uri = outline.uri; - } - - if (!(element instanceof OutlineElement)) { - return true; - } - - const configName = OutlineFilter.kindToConfigName[element.symbol.kind]; - const configKey = `${this._prefix}.${configName}`; - return this._textResourceConfigService.getValue(uri, configKey); - } -} - -export class OutlineItemComparator implements ITreeSorter { - - private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); - - constructor( - public type: OutlineSortOrder = OutlineSortOrder.ByPosition - ) { } - - compare(a: OutlineItem, b: OutlineItem): number { - if (a instanceof OutlineGroup && b instanceof OutlineGroup) { - return a.order - b.order; - - } else if (a instanceof OutlineElement && b instanceof OutlineElement) { - if (this.type === OutlineSortOrder.ByKind) { - return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); - } else if (this.type === OutlineSortOrder.ByName) { - return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); - } else if (this.type === OutlineSortOrder.ByPosition) { - return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); - } - } - return 0; - } -} - -export class OutlineDataSource implements IDataSource { - - getChildren(element: undefined | OutlineModel | OutlineGroup | OutlineElement) { - if (!element) { - return Iterable.empty(); - } - return element.children.values(); - } -} - export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', { dark: foreground, light: foreground, @@ -721,5 +373,4 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (symbolIconVariableColor) { collector.addRule(`${Codicon.symbolVariable.cssSelector} { color: ${symbolIconVariableColor}; }`); } - }); diff --git a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts index 36bfec1737..2fbc13ed4c 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts @@ -16,6 +16,7 @@ import { toMultilineTokens2, SemanticTokensProviderStyling } from 'vs/editor/com import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isSemanticColoringEnabled, SEMANTIC_HIGHLIGHTING_SETTING_ID } from 'vs/editor/common/services/modelServiceImpl'; +import { getDocumentRangeSemanticTokensProvider } from 'vs/editor/common/services/getSemanticTokens'; class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution { @@ -66,11 +67,6 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo })); } - private static _getSemanticColoringProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null { - const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model); - return (result.length > 0 ? result[0] : null); - } - private _cancelAll(): void { for (const request of this._outstandingRequests) { request.cancel(); @@ -92,14 +88,20 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo return; } const model = this._editor.getModel(); - if (model.hasSemanticTokens()) { + if (model.hasCompleteSemanticTokens()) { return; } if (!isSemanticColoringEnabled(model, this._themeService, this._configurationService)) { + if (model.hasSomeSemanticTokens()) { + model.setSemanticTokens(null, false); + } return; } - const provider = ViewportSemanticTokensContribution._getSemanticColoringProvider(model); + const provider = getDocumentRangeSemanticTokensProvider(model); if (!provider) { + if (model.hasSomeSemanticTokens()) { + model.setSemanticTokens(null, false); + } return; } const styling = this._modelService.getSemanticTokensProviderStyling(provider); diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 05931ede9e..f98665fb90 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -110,7 +110,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeft - with selection', () => { @@ -123,7 +123,7 @@ suite('WordOperations', () => { ], {}, (editor) => { editor.setPosition(new Position(5, 2)); cursorWordLeft(editor, true); - assert.deepEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); + assert.deepStrictEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); }); }); @@ -138,7 +138,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeft - issue #48046: Word selection doesn\'t work as usual', () => { @@ -154,7 +154,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordLeftSelect - issue #74369: cursorWordLeft and cursorWordLeftSelect do not behave consistently', () => { @@ -170,7 +170,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordStartLeft', () => { @@ -185,7 +185,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordStartLeft - issue #51119: regression makes VS compatibility impossible', () => { @@ -200,7 +200,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #51275 - cursorWordStartLeft does not push undo/redo stack element', () => { @@ -212,16 +212,16 @@ suite('WordOperations', () => { withTestCodeEditor('', {}, (editor, viewModel) => { type(viewModel, 'foo bar baz'); - assert.equal(editor.getValue(), 'foo bar baz'); + assert.strictEqual(editor.getValue(), 'foo bar baz'); cursorWordStartLeft(editor); cursorWordStartLeft(editor); type(viewModel, 'q'); - assert.equal(editor.getValue(), 'foo qbar baz'); + assert.strictEqual(editor.getValue(), 'foo qbar baz'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(editor.getValue(), 'foo bar baz'); + assert.strictEqual(editor.getValue(), 'foo bar baz'); }); }); @@ -236,7 +236,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - simple', () => { @@ -256,7 +256,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(5, 2)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - selection', () => { @@ -269,7 +269,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { editor.setPosition(new Position(1, 1)); cursorWordRight(editor, true); - assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); + assert.deepStrictEqual(editor.getSelection(), new Selection(1, 1, 1, 8)); }); }); @@ -286,7 +286,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordRight - issue #41199', () => { @@ -302,7 +302,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 17)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('moveWordEndRight', () => { @@ -318,7 +318,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('moveWordStartRight', () => { @@ -335,7 +335,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #51119: cursorWordStartRight regression makes VS compatibility impossible', () => { @@ -350,7 +350,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 15)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #64810: cursorWordStartRight skips first word after newline', () => { @@ -365,7 +365,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(2, 12)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordAccessibilityLeft', () => { @@ -379,7 +379,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordAccessibilityRight', () => { @@ -393,7 +393,7 @@ suite('WordOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 50)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordLeft for non-empty selection', () => { @@ -407,8 +407,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setSelection(new Selection(3, 7, 3, 9)); deleteWordLeft(editor); - assert.equal(model.getLineContent(3), ' Thd Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 7)); + assert.strictEqual(model.getLineContent(3), ' Thd Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 7)); }); }); @@ -423,8 +423,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 1)); deleteWordLeft(editor); - assert.equal(model.getLineContent(1), ' \tMy First Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 1)); + assert.strictEqual(model.getLineContent(1), ' \tMy First Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 1)); }); }); @@ -439,8 +439,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(3, 11)); deleteWordLeft(editor); - assert.equal(model.getLineContent(3), ' Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 5)); + assert.strictEqual(model.getLineContent(3), ' Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 5)); }); }); @@ -455,8 +455,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 11)); deleteWordLeft(editor); - assert.equal(model.getLineContent(2), '\tMy Line'); - assert.deepEqual(editor.getPosition(), new Position(2, 5)); + assert.strictEqual(model.getLineContent(2), '\tMy Line'); + assert.deepStrictEqual(editor.getPosition(), new Position(2, 5)); }); }); @@ -471,8 +471,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 12)); deleteWordLeft(editor); - assert.equal(model.getLineContent(1), ' \tMy st Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 9)); + assert.strictEqual(model.getLineContent(1), ' \tMy st Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 9)); }); }); @@ -487,8 +487,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setSelection(new Selection(3, 7, 3, 9)); deleteWordRight(editor); - assert.equal(model.getLineContent(3), ' Thd Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 7)); + assert.strictEqual(model.getLineContent(3), ' Thd Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 7)); }); }); @@ -503,8 +503,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(5, 3)); deleteWordRight(editor); - assert.equal(model.getLineContent(5), '1'); - assert.deepEqual(editor.getPosition(), new Position(5, 2)); + assert.strictEqual(model.getLineContent(5), '1'); + assert.deepStrictEqual(editor.getPosition(), new Position(5, 2)); }); }); @@ -519,8 +519,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(3, 1)); deleteWordRight(editor); - assert.equal(model.getLineContent(3), 'Third Line🐶'); - assert.deepEqual(editor.getPosition(), new Position(3, 1)); + assert.strictEqual(model.getLineContent(3), 'Third Line🐶'); + assert.deepStrictEqual(editor.getPosition(), new Position(3, 1)); }); }); @@ -535,8 +535,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 5)); deleteWordRight(editor); - assert.equal(model.getLineContent(2), '\tMy Line'); - assert.deepEqual(editor.getPosition(), new Position(2, 5)); + assert.strictEqual(model.getLineContent(2), '\tMy Line'); + assert.deepStrictEqual(editor.getPosition(), new Position(2, 5)); }); }); @@ -551,8 +551,8 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 11)); deleteWordRight(editor); - assert.equal(model.getLineContent(1), ' \tMy Fi Line\t '); - assert.deepEqual(editor.getPosition(), new Position(1, 11)); + assert.strictEqual(model.getLineContent(1), ' \tMy Fi Line\t '); + assert.deepStrictEqual(editor.getPosition(), new Position(1, 11)); }); }); @@ -569,7 +569,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordStartLeft', () => { @@ -585,7 +585,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordEndLeft', () => { @@ -601,7 +601,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordLeft - issue #24947', () => { @@ -611,7 +611,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); withTestCodeEditor([ @@ -620,7 +620,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordStartLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordStartLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); withTestCodeEditor([ @@ -629,7 +629,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordEndLeft(editor); assert.equal(model.getLineContent(1), '{}'); + deleteWordEndLeft(editor); assert.strictEqual(model.getLineContent(1), '{}'); }); }); @@ -644,7 +644,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordRight - issue #3882', () => { @@ -654,7 +654,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -665,7 +665,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordStartRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordStartRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -676,7 +676,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 24)); - deleteWordEndRight(editor); assert.equal(model.getLineContent(1), 'public void Add( int x,int y )', '001'); + deleteWordEndRight(editor); assert.strictEqual(model.getLineContent(1), 'public void Add( int x,int y )', '001'); }); }); @@ -691,7 +691,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordEndRight', () => { @@ -705,7 +705,7 @@ suite('WordOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordRight - issue #3882 (1): Ctrl+Delete removing entire line when used at the end of line', () => { @@ -715,7 +715,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(1, 18)); - deleteWordRight(editor); assert.equal(model.getLineContent(1), 'A line with text.And another one', '001'); + deleteWordRight(editor); assert.strictEqual(model.getLineContent(1), 'A line with text.And another one', '001'); }); }); @@ -726,7 +726,7 @@ suite('WordOperations', () => { ], {}, (editor, _) => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), 'A line with text. And another one', '001'); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'A line with text. And another one', '001'); }); }); @@ -748,7 +748,7 @@ suite('WordOperations', () => { withTestCodeEditor(null, { model }, (editor, _) => { editor.setPosition(new Position(1, 4)); - deleteWordLeft(editor); assert.equal(model.getLineContent(1), 'a '); + deleteWordLeft(editor); assert.strictEqual(model.getLineContent(1), 'a '); }); model.dispose(); @@ -764,7 +764,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(2, 1)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Line1\nLine2'); + assert.strictEqual(model.getValue(), 'Line1\nLine2'); }); }); @@ -775,7 +775,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Justsome text.'); + assert.strictEqual(model.getValue(), 'Justsome text.'); }); }); @@ -786,7 +786,7 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Justsome text.'); + assert.strictEqual(model.getValue(), 'Justsome text.'); }); }); @@ -797,19 +797,19 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 6)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'Just"some text.'); + assert.strictEqual(model.getValue(), 'Just"some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), '"some text.'); + assert.strictEqual(model.getValue(), '"some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'some text.'); + assert.strictEqual(model.getValue(), 'some text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'text.'); + assert.strictEqual(model.getValue(), 'text.'); deleteInsideWord(editor); - assert.equal(model.getValue(), '.'); + assert.strictEqual(model.getValue(), '.'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -820,19 +820,19 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=3+45+6'); + assert.strictEqual(model.getValue(), 'x=3+45+6'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=3++6'); + assert.strictEqual(model.getValue(), 'x=3++6'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x=36'); + assert.strictEqual(model.getValue(), 'x=36'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x='); + assert.strictEqual(model.getValue(), 'x='); deleteInsideWord(editor); - assert.equal(model.getValue(), 'x'); + assert.strictEqual(model.getValue(), 'x'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -843,13 +843,13 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This interesting'); + assert.strictEqual(model.getValue(), 'This interesting'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This'); + assert.strictEqual(model.getValue(), 'This'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); @@ -860,13 +860,13 @@ suite('WordOperations', () => { const model = editor.getModel()!; editor.setPosition(new Position(1, 7)); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This interesting'); + assert.strictEqual(model.getValue(), 'This interesting'); deleteInsideWord(editor); - assert.equal(model.getValue(), 'This'); + assert.strictEqual(model.getValue(), 'This'); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); deleteInsideWord(editor); - assert.equal(model.getValue(), ''); + assert.strictEqual(model.getValue(), ''); }); }); }); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index eb8863cee5..233d7c5a5f 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -55,7 +55,7 @@ export abstract class MoveWordCommand extends EditorCommand { }); model.pushStackElement(); - editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); + editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.Explicit, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); editor.revealPosition(pos, ScrollType.Smooth); diff --git a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts index afc378bc5c..ba4f5b5cea 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/wordPartOperations.test.ts @@ -49,7 +49,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartLeft - issue #53899: whitespace', () => { @@ -63,7 +63,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartLeft - issue #53899: underscores', () => { @@ -77,7 +77,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - basic', () => { @@ -95,7 +95,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(3, 9)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: whitespace', () => { @@ -109,7 +109,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 52)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: underscores', () => { @@ -123,7 +123,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 52)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('cursorWordPartRight - issue #53899: second case', () => { @@ -142,7 +142,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(4, 7)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #93239 - cursorWordPartRight', () => { @@ -158,7 +158,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 8)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('issue #93239 - cursorWordPartLeft', () => { @@ -174,7 +174,7 @@ suite('WordPartOperations', () => { ed => ed.getPosition()!.equals(new Position(1, 1)) ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordPartLeft - basic', () => { @@ -188,7 +188,7 @@ suite('WordPartOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); test('deleteWordPartRight - basic', () => { @@ -202,6 +202,6 @@ suite('WordPartOperations', () => { ed => ed.getValue().length === 0 ); const actual = serializePipePositions(text, actualStops); - assert.deepEqual(actual, EXPECTED); + assert.deepStrictEqual(actual, EXPECTED); }); }); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 4724f66230..8679b05855 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -23,12 +23,13 @@ import 'vs/editor/contrib/find/findController'; import 'vs/editor/contrib/folding/folding'; import 'vs/editor/contrib/fontZoom/fontZoom'; import 'vs/editor/contrib/format/formatActions'; -import 'vs/editor/contrib/gotoSymbol/documentSymbols'; +import 'vs/editor/contrib/documentSymbols/documentSymbols'; import 'vs/editor/contrib/gotoSymbol/goToCommands'; import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/gotoError'; import 'vs/editor/contrib/hover/hover'; import 'vs/editor/contrib/indentation/indentation'; +import 'vs/editor/contrib/inlineHints/inlineHintsController'; import 'vs/editor/contrib/inPlaceReplace/inPlaceReplace'; import 'vs/editor/contrib/linesOperations/linesOperations'; import 'vs/editor/contrib/linkedEditing/linkedEditing'; diff --git a/src/vs/editor/editor.api.ts b/src/vs/editor/editor.api.ts index a6b9649413..7e85429155 100644 --- a/src/vs/editor/editor.api.ts +++ b/src/vs/editor/editor.api.ts @@ -7,6 +7,8 @@ import { EditorOptions, WrappingIndent, EditorAutoIndentStrategy } from 'vs/edit import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import { createMonacoEditorAPI } from 'vs/editor/standalone/browser/standaloneEditor'; import { createMonacoLanguagesAPI } from 'vs/editor/standalone/browser/standaloneLanguages'; +import { globals } from 'vs/base/common/platform'; +import { FormattingConflicts } from 'vs/editor/contrib/format/format'; // Set defaults for standalone editor EditorOptions.wrappingIndent.defaultValue = WrappingIndent.None; @@ -14,6 +16,10 @@ EditorOptions.glyphMargin.defaultValue = false; EditorOptions.autoIndent.defaultValue = EditorAutoIndentStrategy.Advanced; EditorOptions.overviewRulerLanes.defaultValue = 2; +// We need to register a formatter selector which simply picks the first available formatter. +// See https://github.com/microsoft/monaco-editor/issues/2327 +FormattingConflicts.setFormatterSelector((formatter, document, mode) => Promise.resolve(formatter[0])); + const api = createMonacoBaseAPI(); api.editor = createMonacoEditorAPI(); api.languages = createMonacoLanguagesAPI(); @@ -32,7 +38,9 @@ export const Token = api.Token; export const editor = api.editor; export const languages = api.languages; -self.monaco = api; +if (globals.MonacoEnvironment?.globalAPI || globals.define?.amd) { + self.monaco = api; +} if (typeof self.require !== 'undefined' && typeof self.require.config === 'function') { self.require.config({ diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 7af067c874..2e83a3e931 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -42,8 +42,8 @@ export class Colorizer { let text = domNode.firstChild ? domNode.firstChild.nodeValue : ''; domNode.className += ' ' + theme; let render = (str: string) => { - const trustedhtml = ttPolicy ? ttPolicy.createHTML(str) : str; - domNode.innerHTML = trustedhtml as unknown as string; + const trustedhtml = ttPolicy?.createHTML(str) ?? str; + domNode.innerHTML = trustedhtml as string; }; return this.colorize(modeService, text || '', mimeType, options).then(render, (err) => console.error(err)); } @@ -222,7 +222,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: for (let i = 0, length = lines.length; i < length; i++) { let line = lines[i]; - let tokenizeResult = tokenizationSupport.tokenize2(line, state, 0); + let tokenizeResult = tokenizationSupport.tokenize2(line, true, state, 0); LineTokens.convertToEndOffset(tokenizeResult.tokens, line.length); let lineTokens = new LineTokens(tokenizeResult.tokens, line); const isBasicASCII = ViewLineRenderingData.isBasicASCII(line, /* check for basic ASCII */true); diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 56afbc4945..2d1cb530e3 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -137,8 +137,8 @@ function getSafeTokenizationSupport(languageIdentifier: LanguageIdentifier): ITo } return { getInitialState: () => NULL_STATE, - tokenize: (line: string, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), - tokenize2: (line: string, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize(languageIdentifier.language, line, state, deltaOffset), + tokenize2: (line: string, hasEOL: boolean, state: IState, deltaOffset: number) => nullTokenize2(languageIdentifier.id, line, state, deltaOffset) }; } @@ -293,8 +293,8 @@ class InspectTokensWidget extends Disposable implements IContentWidget { private _getTokensAtLine(lineNumber: number): ICompleteLineTokenization { let stateBeforeLine = this._getStateBeforeLine(lineNumber); - let tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), stateBeforeLine, 0); - let tokenizationResult2 = this._tokenizationSupport.tokenize2(this._model.getLineContent(lineNumber), stateBeforeLine, 0); + let tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), true, stateBeforeLine, 0); + let tokenizationResult2 = this._tokenizationSupport.tokenize2(this._model.getLineContent(lineNumber), true, stateBeforeLine, 0); return { startState: stateBeforeLine, @@ -308,7 +308,7 @@ class InspectTokensWidget extends Disposable implements IContentWidget { let state: IState = this._tokenizationSupport.getInitialState(); for (let i = 1; i < lineNumber; i++) { - let tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), state, 0); + let tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), true, state, 0); state = tokenizationResult.endState; } diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts index 3c3b1e974b..4b11d57a9e 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded +import 'vs/editor/contrib/symbolIcons/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import { AbstractGotoSymbolQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 97d06b0b34..cdb6f8cd52 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -176,8 +176,8 @@ export class SimpleEditorProgressService implements IEditorProgressService { return SimpleEditorProgressService.NULL_PROGRESS_RUNNER; } - showWhile(promise: Promise, delay?: number): Promise { - return Promise.resolve(undefined); + async showWhile(promise: Promise, delay?: number): Promise { + await promise; } } @@ -310,14 +310,24 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { this._cachedResolver = null; this._dynamicKeybindings = []; + // for standard keybindings this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - let keyEvent = new StandardKeyboardEvent(e); - let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); + const keyEvent = new StandardKeyboardEvent(e); + const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); if (shouldPreventDefault) { keyEvent.preventDefault(); keyEvent.stopPropagation(); } })); + + // for single modifier chord keybindings (e.g. shift shift) + this._register(dom.addDisposableListener(window, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + const keyEvent = new StandardKeyboardEvent(e); + const shouldPreventDefault = this._singleModifierDispatch(keyEvent, keyEvent.target); + if (shouldPreventDefault) { + keyEvent.preventDefault(); + } + })); } public addDynamicKeybinding(commandId: string, _keybinding: number, handler: ICommandHandler, when: ContextKeyExpression | undefined): IDisposable { @@ -638,12 +648,12 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { return resource && resource.scheme === SimpleWorkspaceContextService.SCHEME; } - public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { + public isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean { return true; } } -export function applyConfigurationValues(configurationService: IConfigurationService, source: any, isDiffEditor: boolean): void { +export function updateConfigurationService(configurationService: IConfigurationService, source: any, isDiffEditor: boolean): void { if (!source) { return; } @@ -736,7 +746,7 @@ export class SimpleUriLabelService implements ILabelService { return basename(resource); } - public getWorkspaceLabel(workspace: IWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { + public getWorkspaceLabel(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace, options?: { verbose: boolean; }): string { return ''; } diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index 0fae1a9e63..d5c2221a3f 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -23,6 +23,19 @@ margin: 0; } +/* +In certain cases, the default positioning of the aria container (left: -999em) can cause scrollbars to appear. +So here we try to avoid that by using a different technique. See https://stackoverflow.com/a/26032207 +*/ +.monaco-aria-container { + position: absolute !important; + height: 1px; + width: 1px; + left: inherit !important; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); +} + /* The hc-black theme is already high contrast optimized */ .monaco-editor.hc-black { -ms-high-contrast-adjust: none; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index d126bc0e7b..098a99dcf1 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -14,7 +14,7 @@ import { InternalEditorAction } from 'vs/editor/common/editorAction'; import { IModelChangedEvent } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { StandaloneKeybindingService, applyConfigurationValues } from 'vs/editor/standalone/browser/simpleServices'; +import { StandaloneKeybindingService, updateConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; import { IMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; @@ -31,6 +31,9 @@ import { StandaloneCodeEditorNLS } from 'vs/editor/common/standaloneStrings'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ILanguageSelection, IModeService } from 'vs/editor/common/services/modeService'; +import { URI } from 'vs/base/common/uri'; /** * Description of an action contribution @@ -227,7 +230,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon constructor( domElement: HTMLElement, - options: IStandaloneEditorConstructionOptions, + _options: Readonly, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @ICommandService commandService: ICommandService, @@ -237,7 +240,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - options = options || {}; + const options = { ..._options }; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; options.ariaLabel = options.ariaLabel + ';' + (StandaloneCodeEditorNLS.accessibilityHelpMessage); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); @@ -353,7 +356,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon constructor( domElement: HTMLElement, - options: IStandaloneEditorConstructionOptions | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -364,11 +367,13 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon @IStandaloneThemeService themeService: IStandaloneThemeService, @INotificationService notificationService: INotificationService, @IConfigurationService configurationService: IConfigurationService, - @IAccessibilityService accessibilityService: IAccessibilityService + @IAccessibilityService accessibilityService: IAccessibilityService, + @IModelService modelService: IModelService, + @IModeService modeService: IModeService, ) { - applyConfigurationValues(configurationService, options, false); + const options = { ..._options }; + updateConfigurationService(configurationService, options, false); const themeDomRegistration = (themeService).registerEditorContainer(domElement); - options = options || {}; if (typeof options.theme === 'string') { themeService.setTheme(options.theme); } @@ -384,7 +389,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon let model: ITextModel | null; if (typeof _model === 'undefined') { - model = (self).monaco.editor.createModel(options.value || '', options.language || 'text/plain'); + model = createTextModel(modelService, modeService, options.value || '', options.language || 'text/plain', undefined); this._ownsModel = true; } else { model = _model; @@ -405,8 +410,8 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon super.dispose(); } - public updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void { - applyConfigurationValues(this._configurationService, newOptions, false); + public updateOptions(newOptions: Readonly): void { + updateConfigurationService(this._configurationService, newOptions, false); if (typeof newOptions.theme === 'string') { this._standaloneThemeService.setTheme(newOptions.theme); } @@ -437,7 +442,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon constructor( domElement: HTMLElement, - options: IDiffEditorConstructionOptions | undefined, + _options: Readonly | undefined, toDispose: IDisposable, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @@ -452,14 +457,14 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon @IEditorProgressService editorProgressService: IEditorProgressService, @IClipboardService clipboardService: IClipboardService, ) { - applyConfigurationValues(configurationService, options, true); + const options = { ..._options }; + updateConfigurationService(configurationService, options, true); const themeDomRegistration = (themeService).registerEditorContainer(domElement); - options = options || {}; if (typeof options.theme === 'string') { options.theme = themeService.setTheme(options.theme); } - super(domElement, options, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); + super(domElement, options, {}, clipboardService, editorWorkerService, contextKeyService, instantiationService, codeEditorService, themeService, notificationService, contextMenuService, editorProgressService); this._contextViewService = contextViewService; this._configurationService = configurationService; @@ -475,15 +480,15 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super.dispose(); } - public updateOptions(newOptions: IDiffEditorOptions & IGlobalEditorOptions): void { - applyConfigurationValues(this._configurationService, newOptions, true); + public updateOptions(newOptions: Readonly): void { + updateConfigurationService(this._configurationService, newOptions, true); if (typeof newOptions.theme === 'string') { this._standaloneThemeService.setTheme(newOptions.theme); } super.updateOptions(newOptions); } - protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: IEditorOptions): CodeEditorWidget { + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly): CodeEditorWidget { return instantiationService.createInstance(StandaloneCodeEditor, container, options); } @@ -507,3 +512,26 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon return this.getModifiedEditor().addAction(descriptor); } } + +/** + * @internal + */ +export function createTextModel(modelService: IModelService, modeService: IModeService, value: string, language: string | undefined, uri: URI | undefined): ITextModel { + value = value || ''; + if (!language) { + const firstLF = value.indexOf('\n'); + let firstLine = value; + if (firstLF !== -1) { + firstLine = value.substring(0, firstLF); + } + return doCreateModel(modelService, value, modeService.createByFilepathOrFirstLine(uri || null, firstLine), uri); + } + return doCreateModel(modelService, value, modeService.create(language), uri); +} + +/** + * @internal + */ +function doCreateModel(modelService: IModelService, value: string, languageSelection: ILanguageSelection, uri: URI | undefined): ITextModel { + return modelService.createModel(value, languageSelection, uri); +} diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 10413b91c5..7d50642dba 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -18,16 +18,16 @@ import { FindMatch, ITextModel, TextModelResolvedOptions } from 'vs/editor/commo import * as modes from 'vs/editor/common/modes'; import { NULL_STATE, nullTokenize } from 'vs/editor/common/modes/nullMode'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { ILanguageSelection } from 'vs/editor/common/services/modeService'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IWebWorkerOptions, MonacoWebWorker, createWebWorker as actualCreateWebWorker } from 'vs/editor/common/services/webWorker'; import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums'; import { Colorizer, IColorizerElementOptions, IColorizerOptions } from 'vs/editor/standalone/browser/colorizer'; import { SimpleEditorModelResolverService } from 'vs/editor/standalone/browser/simpleServices'; -import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor'; +import { IDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor, StandaloneDiffEditor, StandaloneEditor, createTextModel } from 'vs/editor/standalone/browser/standaloneCodeEditor'; import { DynamicStandaloneServices, IEditorOverrideServices, StaticServices } from 'vs/editor/standalone/browser/standaloneServices'; import { IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneThemeService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -42,6 +42,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl'; import { splitLines } from 'vs/base/common/strings'; +import { IModelService } from 'vs/editor/common/services/modelService'; type Omit = Pick>; @@ -87,7 +88,9 @@ export function create(domElement: HTMLElement, options?: IStandaloneEditorConst services.get(IStandaloneThemeService), services.get(INotificationService), services.get(IConfigurationService), - services.get(IAccessibilityService) + services.get(IAccessibilityService), + services.get(IModelService), + services.get(IModeService), ); }); } @@ -140,27 +143,18 @@ export function createDiffNavigator(diffEditor: IStandaloneDiffEditor, opts?: ID return new DiffNavigator(diffEditor, opts); } -function doCreateModel(value: string, languageSelection: ILanguageSelection, uri?: URI): ITextModel { - return StaticServices.modelService.get().createModel(value, languageSelection, uri); -} - /** * Create a new editor model. * You can specify the language that should be set for this model or let the language be inferred from the `uri`. */ export function createModel(value: string, language?: string, uri?: URI): ITextModel { - value = value || ''; - - if (!language) { - let firstLF = value.indexOf('\n'); - let firstLine = value; - if (firstLF !== -1) { - firstLine = value.substring(0, firstLF); - } - - return doCreateModel(value, StaticServices.modeService.get().createByFilepathOrFirstLine(uri || null, firstLine), uri); - } - return doCreateModel(value, StaticServices.modeService.get().create(language), uri); + return createTextModel( + StaticServices.modelService.get(), + StaticServices.modeService.get(), + value, + language, + uri + ); } /** @@ -188,6 +182,14 @@ export function getModelMarkers(filter: { owner?: string, resource?: URI, take?: return StaticServices.markerService.get().read(filter); } +/** + * Emitted when markers change for a model. + * @event + */ +export function onDidChangeMarkers(listener: (e: readonly URI[]) => void): IDisposable { + return StaticServices.markerService.get().onMarkerChanged(listener); +} + /** * Get the model that has `uri` if it exists. */ @@ -276,7 +278,7 @@ function getSafeTokenizationSupport(language: string): Omit NULL_STATE, - tokenize: (line: string, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset) + tokenize: (line: string, hasEOL: boolean, state: modes.IState, deltaOffset: number) => nullTokenize(language, line, state, deltaOffset) }; } @@ -294,7 +296,7 @@ export function tokenize(text: string, languageId: string): Token[][] { let state = tokenizationSupport.getInitialState(); for (let i = 0, len = lines.length; i < len; i++) { let line = lines[i]; - let tokenizationResult = tokenizationSupport.tokenize(line, state, 0); + let tokenizationResult = tokenizationSupport.tokenize(line, true, state, 0); result[i] = tokenizationResult.tokens; state = tokenizationResult.endState; @@ -323,6 +325,13 @@ export function remeasureFonts(): void { clearAllFontInfos(); } +/** + * Register a command. + */ +export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable { + return CommandsRegistry.registerCommand({ id, handler }); +} + /** * @internal */ @@ -338,6 +347,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { setModelLanguage: setModelLanguage, setModelMarkers: setModelMarkers, getModelMarkers: getModelMarkers, + onDidChangeMarkers: onDidChangeMarkers, getModels: getModels, getModel: getModel, onDidCreateModel: onDidCreateModel, @@ -353,6 +363,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor { defineTheme: defineTheme, setTheme: setTheme, remeasureFonts: remeasureFonts, + registerCommand: registerCommand, // enums AccessibilitySupport: standaloneEnums.AccessibilitySupport, @@ -363,7 +374,6 @@ export function createMonacoEditorAPI(): typeof monaco.editor { EditorOption: standaloneEnums.EditorOption, EndOfLinePreference: standaloneEnums.EndOfLinePreference, EndOfLineSequence: standaloneEnums.EndOfLineSequence, - InDiffEditorState: standaloneEnums.InDiffEditorState, MinimapPosition: standaloneEnums.MinimapPosition, MouseTargetType: standaloneEnums.MouseTargetType, OverlayWidgetPositionPreference: standaloneEnums.OverlayWidgetPositionPreference, diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 5084730ef5..a7698954aa 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -67,7 +68,7 @@ export function setLanguageConfiguration(languageId: string, configuration: Lang if (!languageIdentifier) { throw new Error(`Cannot set configuration for unknown language ${languageId}`); } - return LanguageConfigurationRegistry.register(languageIdentifier, configuration); + return LanguageConfigurationRegistry.register(languageIdentifier, configuration, 100); } /** @@ -75,9 +76,11 @@ export function setLanguageConfiguration(languageId: string, configuration: Lang */ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSupport { + private readonly _languageIdentifier: modes.LanguageIdentifier; private readonly _actual: EncodedTokensProvider; - constructor(actual: EncodedTokensProvider) { + constructor(languageIdentifier: modes.LanguageIdentifier, actual: EncodedTokensProvider) { + this._languageIdentifier = languageIdentifier; this._actual = actual; } @@ -85,11 +88,14 @@ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSu return this._actual.getInitialState(); } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { + if (typeof this._actual.tokenize === 'function') { + return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, <{ tokenize(line: string, state: modes.IState): ILineTokens; }>this._actual, line, state, offsetDelta); + } throw new Error('Not supported!'); } - public tokenize2(line: string, state: modes.IState): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 { let result = this._actual.tokenizeEncoded(line, state); return new TokenizationResult2(result.tokens, result.endState); } @@ -114,7 +120,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return this._actual.getInitialState(); } - private _toClassicTokens(tokens: IToken[], language: string, offsetDelta: number): Token[] { + private static _toClassicTokens(tokens: IToken[], language: string, offsetDelta: number): Token[] { let result: Token[] = []; let previousStartIndex: number = 0; for (let i = 0, len = tokens.length; i < len; i++) { @@ -137,9 +143,9 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return result; } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { - let actualResult = this._actual.tokenize(line, state); - let tokens = this._toClassicTokens(actualResult.tokens, this._languageIdentifier.language, offsetDelta); + public static adaptTokenize(language: string, actual: { tokenize(line: string, state: modes.IState): ILineTokens; }, line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + let actualResult = actual.tokenize(line, state); + let tokens = TokenizationSupport2Adapter._toClassicTokens(actualResult.tokens, language, offsetDelta); let endState: modes.IState; // try to save an object if possible @@ -152,6 +158,10 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return new TokenizationResult(tokens, endState); } + public tokenize(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult { + return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, this._actual, line, state, offsetDelta); + } + private _toBinaryTokens(tokens: IToken[], offsetDelta: number): Uint32Array { const languageId = this._languageIdentifier.id; const tokenTheme = this._standaloneThemeService.getColorTheme().tokenTheme; @@ -190,7 +200,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return actualResult; } - public tokenize2(line: string, state: modes.IState, offsetDelta: number): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, state: modes.IState, offsetDelta: number): TokenizationResult2 { let actualResult = this._actual.tokenize(line, state); let tokens = this._toBinaryTokens(actualResult.tokens, offsetDelta); @@ -287,6 +297,10 @@ export interface EncodedTokensProvider { * Tokenize a line given the state at the beginning of the line. */ tokenizeEncoded(line: string, state: modes.IState): IEncodedLineTokens; + /** + * Tokenize a line given the state at the beginning of the line. + */ + tokenize?(line: string, state: modes.IState): ILineTokens; } function isEncodedTokensProvider(provider: TokensProvider | EncodedTokensProvider): provider is EncodedTokensProvider { @@ -297,6 +311,22 @@ function isThenable(obj: any): obj is Thenable { return obj && typeof obj.then === 'function'; } +/** + * Change the color map that is used for token colors. + * Supported formats (hex): #RRGGBB, $RRGGBBAA, #RGB, #RGBA + */ +export function setColorMap(colorMap: string[] | null): void { + if (colorMap) { + const result: Color[] = [null!]; + for (let i = 1, len = colorMap.length; i < len; i++) { + result[i] = Color.fromHex(colorMap[i]); + } + StaticServices.standaloneThemeService.get().setColorMapOverride(result); + } else { + StaticServices.standaloneThemeService.get().setColorMapOverride(null); + } +} + /** * Set the tokens provider for a language (manual implementation). */ @@ -307,7 +337,7 @@ export function setTokensProvider(languageId: string, provider: TokensProvider | } const create = (provider: TokensProvider | EncodedTokensProvider) => { if (isEncodedTokensProvider(provider)) { - return new EncodedTokenizationSupport2Adapter(provider); + return new EncodedTokenizationSupport2Adapter(languageIdentifier!, provider); } else { return new TokenizationSupport2Adapter(StaticServices.standaloneThemeService.get(), languageIdentifier!, provider); } @@ -557,6 +587,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { // provider methods setLanguageConfiguration: setLanguageConfiguration, + setColorMap: setColorMap, setTokensProvider: setTokensProvider, setMonarchTokensProvider: setMonarchTokensProvider, registerReferenceProvider: registerReferenceProvider, diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index e6af3cf8f2..fcc49d27c4 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -16,7 +16,7 @@ import { ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/c import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IThemingRegistry, ITokenStyle } from 'vs/platform/theme/common/themeService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; const VS_THEME_NAME = 'vs'; const VS_DARK_THEME_NAME = 'vs-dark'; @@ -39,7 +39,11 @@ class StandaloneTheme implements IStandaloneTheme { this.themeData = standaloneThemeData; let base = standaloneThemeData.base; if (name.length > 0) { - this.id = base + ' ' + name; + if (isBuiltinTheme(name)) { + this.id = name; + } else { + this.id = base + ' ' + name; + } this.themeName = name; } else { this.id = base; @@ -199,6 +203,7 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon private _allCSS: string; private _globalStyleElement: HTMLStyleElement | null; private _styleElements: HTMLStyleElement[]; + private _colorMapOverride: Color[] | null; private _theme!: IStandaloneTheme; constructor() { @@ -209,17 +214,18 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME)); this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME)); - const iconRegistry = getIconRegistry(); + const iconsStyleSheet = getIconsStyleSheet(); - this._codiconCSS = iconRegistry.getCSS(); + this._codiconCSS = iconsStyleSheet.getCSS(); this._themeCSS = ''; this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`; this._globalStyleElement = null; this._styleElements = []; + this._colorMapOverride = null; this.setTheme(VS_THEME_NAME); - iconRegistry.onDidChange(() => { - this._codiconCSS = iconRegistry.getCSS(); + iconsStyleSheet.onDidChange(() => { + this._codiconCSS = iconsStyleSheet.getCSS(); this._updateCSS(); }); } @@ -284,6 +290,11 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon return this._theme; } + public setColorMapOverride(colorMapOverride: Color[] | null): void { + this._colorMapOverride = colorMapOverride; + this._updateThemeOrColorMap(); + } + public setTheme(themeName: string): string { let theme: StandaloneTheme; if (this._knownThemes.has(themeName)) { @@ -296,7 +307,11 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon return theme.id; } this._theme = theme; + this._updateThemeOrColorMap(); + return theme.id; + } + private _updateThemeOrColorMap(): void { let cssRules: string[] = []; let hasRule: { [rule: string]: boolean; } = {}; let ruleCollector: ICssStyleCollector = { @@ -307,19 +322,16 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon } } }; - themingRegistry.getThemingParticipants().forEach(p => p(theme, ruleCollector, this._environment)); + themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment)); - let tokenTheme = theme.tokenTheme; - let colorMap = tokenTheme.getColorMap(); + const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); this._themeCSS = cssRules.join('\n'); this._updateCSS(); TokenizationRegistry.setColorMap(colorMap); - this._onColorThemeChange.fire(theme); - - return theme.id; + this._onColorThemeChange.fire(this._theme); } private _updateCSS(): void { diff --git a/src/vs/editor/standalone/common/monarch/monarchCommon.ts b/src/vs/editor/standalone/common/monarch/monarchCommon.ts index bb437f9a8a..e6d92d3e0f 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCommon.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCommon.ts @@ -22,6 +22,7 @@ export const enum MonarchBracket { export interface ILexerMin { languageId: string; + includeLF: boolean; noThrow: boolean; ignoreCase: boolean; unicode: boolean; diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index cda7ec8418..8ee8458d66 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -81,12 +81,21 @@ function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): /** * Compiles a regular expression string, adding the 'i' flag if 'ignoreCase' is set, and the 'u' flag if 'unicode' is set. * Also replaces @\w+ or sequences with the content of the specified attribute + * @\w+ replacement can be avoided by escaping `@` signs with another `@` sign. + * @example /@attr/ will be replaced with the value of lexer[attr] + * @example /@@text/ will not be replaced and will become /@text/. */ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { let n = 0; - while (str.indexOf('@') >= 0 && n < 5) { // at most 5 expansions - n++; - str = str.replace(/@(\w+)/g, function (s, attr?) { + let hadExpansion: boolean; + do { + hadExpansion = false; + str = str.replace(/(.|^)@(\w+)/g, function (s, charBeforeAtSign, attr?) { + if (charBeforeAtSign === '@') { + // do not expand @@ + return s; + } + hadExpansion = true; let sub = ''; if (typeof (lexer[attr]) === 'string') { sub = lexer[attr]; @@ -99,9 +108,13 @@ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { throw monarchCommon.createError(lexer, 'attribute reference \'' + attr + '\' must be a string, used at: ' + str); } } - return (monarchCommon.empty(sub) ? '' : '(?:' + sub + ')'); + return charBeforeAtSign + (monarchCommon.empty(sub) ? '' : '(?:' + sub + ')'); }); - } + n++; + } while (hadExpansion && n < 5); + + // handle escaped @@ + str = str.replace(/@@/g, '@'); let flags = (lexer.ignoreCase ? 'i' : '') + (lexer.unicode ? 'u' : ''); return new RegExp(str, flags); @@ -395,6 +408,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // Create our lexer let lexer: monarchCommon.ILexer = {}; lexer.languageId = languageId; + lexer.includeLF = bool(json.includeLF, false); lexer.noThrow = false; // raise exceptions during compilation lexer.maxStack = 100; @@ -411,6 +425,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // For calling compileAction later on let lexerMin: monarchCommon.ILexerMin = json; lexerMin.languageId = languageId; + lexerMin.includeLF = lexer.includeLF; lexerMin.ignoreCase = lexer.ignoreCase; lexerMin.unicode = lexer.unicode; lexerMin.noThrow = lexer.noThrow; diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index 28d9f5b6d0..92d61afeab 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -231,7 +231,7 @@ class MonarchLineState implements modes.IState { interface IMonarchTokensCollector { enterMode(startOffset: number, modeId: string): void; emit(startOffset: number, type: string): void; - nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; + nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState; } class MonarchClassicTokensCollector implements IMonarchTokensCollector { @@ -261,7 +261,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { this._tokens.push(new Token(startOffset, type, this._language!)); } - public nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { + public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { const nestedModeId = embeddedModeData.modeId; const embeddedModeState = embeddedModeData.state; @@ -272,7 +272,7 @@ class MonarchClassicTokensCollector implements IMonarchTokensCollector { return embeddedModeState; } - let nestedResult = nestedModeTokenizationSupport.tokenize(embeddedModeLine, embeddedModeState, offsetDelta); + let nestedResult = nestedModeTokenizationSupport.tokenize(embeddedModeLine, hasEOL, embeddedModeState, offsetDelta); this._tokens = this._tokens.concat(nestedResult.tokens); this._lastTokenType = null; this._lastTokenLanguage = null; @@ -345,7 +345,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { return result; } - public nestedModeTokenize(embeddedModeLine: string, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { + public nestedModeTokenize(embeddedModeLine: string, hasEOL: boolean, embeddedModeData: EmbeddedModeData, offsetDelta: number): modes.IState { const nestedModeId = embeddedModeData.modeId; const embeddedModeState = embeddedModeData.state; @@ -356,7 +356,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { return embeddedModeState; } - let nestedResult = nestedModeTokenizationSupport.tokenize2(embeddedModeLine, embeddedModeState, offsetDelta); + let nestedResult = nestedModeTokenizationSupport.tokenize2(embeddedModeLine, hasEOL, embeddedModeState, offsetDelta); this._prependTokens = MonarchModernTokensCollector._merge(this._prependTokens, this._tokens, nestedResult.tokens); this._tokens = []; this._currentLanguageId = 0; @@ -456,23 +456,23 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return MonarchLineStateFactory.create(rootState, null); } - public tokenize(line: string, lineState: modes.IState, offsetDelta: number): TokenizationResult { + public tokenize(line: string, hasEOL: boolean, lineState: modes.IState, offsetDelta: number): TokenizationResult { let tokensCollector = new MonarchClassicTokensCollector(); - let endLineState = this._tokenize(line, lineState, offsetDelta, tokensCollector); + let endLineState = this._tokenize(line, hasEOL, lineState, offsetDelta, tokensCollector); return tokensCollector.finalize(endLineState); } - public tokenize2(line: string, lineState: modes.IState, offsetDelta: number): TokenizationResult2 { + public tokenize2(line: string, hasEOL: boolean, lineState: modes.IState, offsetDelta: number): TokenizationResult2 { let tokensCollector = new MonarchModernTokensCollector(this._modeService, this._standaloneThemeService.getColorTheme().tokenTheme); - let endLineState = this._tokenize(line, lineState, offsetDelta, tokensCollector); + let endLineState = this._tokenize(line, hasEOL, lineState, offsetDelta, tokensCollector); return tokensCollector.finalize(endLineState); } - private _tokenize(line: string, lineState: MonarchLineState, offsetDelta: number, collector: IMonarchTokensCollector): MonarchLineState { + private _tokenize(line: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, collector: IMonarchTokensCollector): MonarchLineState { if (lineState.embeddedModeData) { - return this._nestedTokenize(line, lineState, offsetDelta, collector); + return this._nestedTokenize(line, hasEOL, lineState, offsetDelta, collector); } else { - return this._myTokenize(line, lineState, offsetDelta, collector); + return this._myTokenize(line, hasEOL, lineState, offsetDelta, collector); } } @@ -518,24 +518,24 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return popOffset; } - private _nestedTokenize(line: string, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { + private _nestedTokenize(line: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { let popOffset = this._findLeavingNestedModeOffset(line, lineState); if (popOffset === -1) { // tokenization will not leave nested mode - let nestedEndState = tokensCollector.nestedModeTokenize(line, lineState.embeddedModeData!, offsetDelta); + let nestedEndState = tokensCollector.nestedModeTokenize(line, hasEOL, lineState.embeddedModeData!, offsetDelta); return MonarchLineStateFactory.create(lineState.stack, new EmbeddedModeData(lineState.embeddedModeData!.modeId, nestedEndState)); } let nestedModeLine = line.substring(0, popOffset); if (nestedModeLine.length > 0) { // tokenize with the nested mode - tokensCollector.nestedModeTokenize(nestedModeLine, lineState.embeddedModeData!, offsetDelta); + tokensCollector.nestedModeTokenize(nestedModeLine, false, lineState.embeddedModeData!, offsetDelta); } let restOfTheLine = line.substring(popOffset); - return this._myTokenize(restOfTheLine, lineState, offsetDelta + popOffset, tokensCollector); + return this._myTokenize(restOfTheLine, hasEOL, lineState, offsetDelta + popOffset, tokensCollector); } private _safeRuleName(rule: monarchCommon.IRule | null): string { @@ -545,9 +545,11 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return '(unknown)'; } - private _myTokenize(line: string, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { + private _myTokenize(lineWithoutLF: string, hasEOL: boolean, lineState: MonarchLineState, offsetDelta: number, tokensCollector: IMonarchTokensCollector): MonarchLineState { tokensCollector.enterMode(offsetDelta, this._modeId); + const lineWithoutLFLength = lineWithoutLF.length; + const line = (hasEOL && this._lexer.includeLF ? lineWithoutLF + '\n' : lineWithoutLF); const lineLength = line.length; let embeddedModeData = lineState.embeddedModeData; @@ -563,7 +565,7 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { } let groupMatching: GroupMatching | null = null; - // See https://github.com/microsoft/monaco-editor/issues/1235: + // See https://github.com/microsoft/monaco-editor/issues/1235 // Evaluate rules at least once for an empty line let forceEvaluation = true; @@ -752,8 +754,8 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { if (pos < lineLength) { // there is content from the embedded mode on this line - const restOfLine = line.substr(pos); - return this._nestedTokenize(restOfLine, MonarchLineStateFactory.create(stack, embeddedModeData), offsetDelta + pos, tokensCollector); + const restOfLine = lineWithoutLF.substr(pos); + return this._nestedTokenize(restOfLine, hasEOL, MonarchLineStateFactory.create(stack, embeddedModeData), offsetDelta + pos, tokensCollector); } else { return MonarchLineStateFactory.create(stack, embeddedModeData); } @@ -831,7 +833,9 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { tokenType = monarchCommon.sanitize(token); } - tokensCollector.emit(pos0 + offsetDelta, tokenType); + if (pos0 < lineWithoutLFLength) { + tokensCollector.emit(pos0 + offsetDelta, tokenType); + } } if (enteringEmbeddedMode !== null) { diff --git a/src/vs/editor/standalone/common/monarch/monarchTypes.ts b/src/vs/editor/standalone/common/monarch/monarchTypes.ts index a48a92faa9..affb17e42a 100644 --- a/src/vs/editor/standalone/common/monarch/monarchTypes.ts +++ b/src/vs/editor/standalone/common/monarch/monarchTypes.ts @@ -41,6 +41,15 @@ export interface IMonarchLanguage { * attach this to every token class (by default '.' + name) */ tokenPostfix?: string; + /** + * include line feeds (in the form of a \n character) at the end of lines + * Defaults to false + */ + includeLF?: boolean; + /** + * Other keys that can be referred to by the tokenizer. + */ + [key: string]: any; } /** diff --git a/src/vs/editor/standalone/common/standaloneThemeService.ts b/src/vs/editor/standalone/common/standaloneThemeService.ts index ba39c4479c..31794bbad2 100644 --- a/src/vs/editor/standalone/common/standaloneThemeService.ts +++ b/src/vs/editor/standalone/common/standaloneThemeService.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Color } from 'vs/base/common/color'; import { ITokenThemeRule, TokenTheme } from 'vs/editor/common/modes/supports/tokenization'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -33,4 +34,7 @@ export interface IStandaloneThemeService extends IThemeService { defineTheme(themeName: string, themeData: IStandaloneThemeData): void; getColorTheme(): IStandaloneTheme; + + setColorMapOverride(colorMapOverride: Color[] | null): void; + } diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index 55ef63eaa8..b9c4885b03 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -65,7 +65,7 @@ export const vs: IStandaloneThemeData = { { token: 'operator.scss', foreground: '666666' }, { token: 'operator.sql', foreground: '778899' }, { token: 'operator.swift', foreground: '666666' }, - { token: 'predefined.sql', foreground: 'FF00FF' }, + { token: 'predefined.sql', foreground: 'C700C7' }, ], colors: { [editorBackground]: '#FFFFFE', diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 7b8db90102..31e5f30033 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -68,7 +68,8 @@ suite('TokenizationSupport2Adapter', () => { tokenColorMap: [] }; } - + setColorMapOverride(colorMapOverride: Color[] | null): void { + } public getFileIconTheme(): IFileIconTheme { return { hasFileIcons: false, @@ -107,15 +108,15 @@ suite('TokenizationSupport2Adapter', () => { const adapter = new TokenizationSupport2Adapter(new MockThemeService(), languageIdentifier, new BadTokensProvider()); - const actualClassicTokens = adapter.tokenize('whatever', MockState.INSTANCE, offsetDelta); - assert.deepEqual(actualClassicTokens.tokens, expectedClassicTokens); + const actualClassicTokens = adapter.tokenize('whatever', true, MockState.INSTANCE, offsetDelta); + assert.deepStrictEqual(actualClassicTokens.tokens, expectedClassicTokens); - const actualModernTokens = adapter.tokenize2('whatever', MockState.INSTANCE, offsetDelta); + const actualModernTokens = adapter.tokenize2('whatever', true, MockState.INSTANCE, offsetDelta); const modernTokens: number[] = []; for (let i = 0; i < actualModernTokens.tokens.length; i++) { modernTokens[i] = actualModernTokens.tokens[i]; } - assert.deepEqual(modernTokens, expectedModernTokens); + assert.deepStrictEqual(modernTokens, expectedModernTokens); } test('tokens always start at index 0 (no offset delta)', () => { diff --git a/src/vs/editor/standalone/test/monarch/monarch.test.ts b/src/vs/editor/standalone/test/monarch/monarch.test.ts index a6e9956dab..bcc7a089b1 100644 --- a/src/vs/editor/standalone/test/monarch/monarch.test.ts +++ b/src/vs/editor/standalone/test/monarch/monarch.test.ts @@ -19,6 +19,17 @@ suite('Monarch', () => { return new MonarchTokenizer(modeService, null!, languageId, compile(languageId, language)); } + function getTokens(tokenizer: MonarchTokenizer, lines: string[]): Token[][] { + const actualTokens: Token[][] = []; + let state = tokenizer.getInitialState(); + for (const line of lines) { + const result = tokenizer.tokenize(line, true, state, 0); + actualTokens.push(result.tokens); + state = result.endState; + } + return actualTokens; + } + test('Ensure @rematch and nextEmbedded can be used together in Monarch grammar', () => { const modeService = new ModeServiceImpl(); const innerModeRegistration = ModesRegistry.registerLanguage({ @@ -65,42 +76,192 @@ suite('Monarch', () => { `""")`, ]; - const actualTokens: Token[][] = []; - let state = tokenizer.getInitialState(); - for (const line of lines) { - const result = tokenizer.tokenize(line, state, 0); - actualTokens.push(result.tokens); - state = result.endState; - } + const actualTokens = getTokens(tokenizer, lines); - assert.deepEqual(actualTokens, [ + assert.deepStrictEqual(actualTokens, [ [ - { 'offset': 0, 'type': 'source.test1', 'language': 'test1' }, - { 'offset': 12, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 15, 'type': 'token.sql', 'language': 'sql' }, - { 'offset': 61, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 64, 'type': 'source.test1', 'language': 'test1' } + new Token(0, 'source.test1', 'test1'), + new Token(12, 'string.quote.test1', 'test1'), + new Token(15, 'token.sql', 'sql'), + new Token(61, 'string.quote.test1', 'test1'), + new Token(64, 'source.test1', 'test1') ], [ - { 'offset': 0, 'type': 'source.test1', 'language': 'test1' }, - { 'offset': 12, 'type': 'string.quote.test1', 'language': 'test1' } + new Token(0, 'source.test1', 'test1'), + new Token(12, 'string.quote.test1', 'test1') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'token.sql', 'language': 'sql' } + new Token(0, 'token.sql', 'sql') ], [ - { 'offset': 0, 'type': 'string.quote.test1', 'language': 'test1' }, - { 'offset': 3, 'type': 'source.test1', 'language': 'test1' } + new Token(0, 'string.quote.test1', 'test1'), + new Token(3, 'source.test1', 'test1') ] ]); innerModeTokenizationRegistration.dispose(); innerModeRegistration.dispose(); }); + test('microsoft/monaco-editor#1235: Empty Line Handling', () => { + const modeService = new ModeServiceImpl(); + const tokenizer = createMonarchTokenizer(modeService, 'test', { + tokenizer: { + root: [ + { include: '@comments' }, + ], + + comments: [ + [/\/\/$/, 'comment'], // empty single-line comment + [/\/\//, 'comment', '@comment_cpp'], + ], + + comment_cpp: [ + [/(?:[^\\]|(?:\\.))+$/, 'comment', '@pop'], + [/.+$/, 'comment'], + [/$/, 'comment', '@pop'] + // No possible rule to detect an empty line and @pop? + ], + }, + }); + + const lines = [ + `// This comment \\`, + ` continues on the following line`, + ``, + `// This comment does NOT continue \\\\`, + ` because the escape char was itself escaped`, + ``, + `// This comment DOES continue because \\\\\\`, + ` the 1st '\\' escapes the 2nd; the 3rd escapes EOL`, + ``, + `// This comment continues to the following line \\`, + ``, + `But the line was empty. This line should not be commented.`, + ]; + + const actualTokens = getTokens(tokenizer, lines); + + assert.deepStrictEqual(actualTokens, [ + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'source.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'comment.test', 'test')], + [], + [new Token(0, 'source.test', 'test')] + ]); + + }); + + test('microsoft/monaco-editor#2265: Exit a state at end of line', () => { + const modeService = new ModeServiceImpl(); + const tokenizer = createMonarchTokenizer(modeService, 'test', { + includeLF: true, + tokenizer: { + root: [ + [/^\*/, '', '@inner'], + [/\:\*/, '', '@inner'], + [/[^*:]+/, 'string'], + [/[*:]/, 'string'] + ], + inner: [ + [/\n/, '', '@pop'], + [/\d+/, 'number'], + [/[^\d]+/, ''] + ] + } + }); + + const lines = [ + `PRINT 10 * 20`, + `*FX200, 3`, + `PRINT 2*3:*FX200, 3` + ]; + + const actualTokens = getTokens(tokenizer, lines); + + assert.deepStrictEqual(actualTokens, [ + [ + new Token(0, 'string.test', 'test'), + ], + [ + new Token(0, '', 'test'), + new Token(3, 'number.test', 'test'), + new Token(6, '', 'test'), + new Token(8, 'number.test', 'test'), + ], + [ + new Token(0, 'string.test', 'test'), + new Token(9, '', 'test'), + new Token(13, 'number.test', 'test'), + new Token(16, '', 'test'), + new Token(18, 'number.test', 'test'), + ] + ]); + }); + + test('issue #115662: monarchCompile function need an extra option which can control replacement', () => { + const modeService = new ModeServiceImpl(); + + const tokenizer1 = createMonarchTokenizer(modeService, 'test', { + ignoreCase: false, + uselessReplaceKey1: '@uselessReplaceKey2', + uselessReplaceKey2: '@uselessReplaceKey3', + uselessReplaceKey3: '@uselessReplaceKey4', + uselessReplaceKey4: '@uselessReplaceKey5', + uselessReplaceKey5: '@ham' || '', + tokenizer: { + root: [ + { + regex: /@\w+/.test('@ham') + ? new RegExp(`^${'@uselessReplaceKey1'}$`) + : new RegExp(`^${'@ham'}$`), + action: { token: 'ham' } + }, + ], + }, + }); + + const tokenizer2 = createMonarchTokenizer(modeService, 'test', { + ignoreCase: false, + tokenizer: { + root: [ + { + regex: /@@ham/, + action: { token: 'ham' } + }, + ], + }, + }); + + const lines = [ + `@ham` + ]; + + const actualTokens1 = getTokens(tokenizer1, lines); + assert.deepStrictEqual(actualTokens1, [ + [ + new Token(0, 'ham.test', 'test'), + ] + ]); + + const actualTokens2 = getTokens(tokenizer2, lines); + assert.deepStrictEqual(actualTokens2, [ + [ + new Token(0, 'ham.test', 'test'), + ] + ]); + }); + }); diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index ce01b401af..de54c9ea2e 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -964,7 +964,7 @@ suite('Editor Commands - ShiftCommand', () => { autoIndent: EditorAutoIndentStrategy.Full, }); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } @@ -979,7 +979,7 @@ suite('Editor Commands - ShiftCommand', () => { autoIndent: EditorAutoIndentStrategy.Full, }); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } }); diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index ec2352477e..1fa9c9a59b 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -19,10 +19,10 @@ function testCommand(lines: string[], selections: Selection[], edits: IIdentifie model.applyEdits(edits); - assert.deepEqual(model.getLinesContent(), expectedLines); + assert.deepStrictEqual(model.getLinesContent(), expectedLines); let actualSelections = viewModel.getSelections(); - assert.deepEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); + assert.deepStrictEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); } @@ -202,7 +202,7 @@ suite('SideEditing', () => { forceMoveMarkers: editForceMoveMarkers }]); const actual = viewModel.getSelection(); - assert.deepEqual(actual.toString(), expected.toString(), msg); + assert.deepStrictEqual(actual.toString(), expected.toString(), msg); }); } diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index 5309f1afb2..9f7708d4dc 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -37,14 +37,14 @@ function assertTrimTrailingWhitespaceCommand(text: string[], expected: IIdentifi return withEditorModel(text, (model) => { let op = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), []); let actual = getEditOperation(model, op); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } function assertTrimTrailingWhitespace(text: string[], cursors: Position[], expected: IIdentifiedSingleEditOperation[]): void { return withEditorModel(text, (model) => { let actual = trimTrailingWhitespace(model, cursors); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 3a643a9972..ed3bcc8c3f 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -115,7 +115,7 @@ function assertCursor(viewModel: ViewModel, what: Position | Selection | Selecti let actual = viewModel.getSelections().map(s => s.toString()); let expected = selections.map(s => s.toString()); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } suite('Editor Controller - Cursor', () => { @@ -795,11 +795,11 @@ suite('Editor Controller - Cursor', () => { viewModel.onEvent((e) => { if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { events++; - assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + assert.deepStrictEqual(e.selections, [new Selection(1, 2, 1, 2)]); } }); moveTo(editor, viewModel, 1, 2); - assert.equal(events, 1, 'receives 1 event'); + assert.strictEqual(events, 1, 'receives 1 event'); }); }); @@ -809,11 +809,11 @@ suite('Editor Controller - Cursor', () => { viewModel.onEvent((e) => { if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { events++; - assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + assert.deepStrictEqual(e.selections, [new Selection(1, 1, 1, 2)]); } }); moveTo(editor, viewModel, 1, 2, true); - assert.equal(events, 1, 'receives 1 event'); + assert.strictEqual(events, 1, 'receives 1 event'); }); }); @@ -1311,7 +1311,7 @@ suite('Editor Controller - Regression tests', () => { // Check that indenting maintains the selection start at column 1 CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); }); model.dispose(); @@ -1330,49 +1330,49 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); viewModel.type('x'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert6'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert6'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert7'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert7'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert8'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert8'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert9'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert10'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert11'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert12'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t\nx', 'assert13'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert14'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert15'); }); model.dispose(); @@ -1387,13 +1387,13 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Position(1, 1)); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.getValue(), 'Hello\nworld'); + assert.strictEqual(model.getValue(), 'Hello\nworld'); model.pushEOL(EndOfLineSequence.CRLF); - assert.equal(model.getValue(), 'Hello\r\nworld'); + assert.strictEqual(model.getValue(), 'Hello\r\nworld'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'Hello\nworld'); + assert.strictEqual(model.getValue(), 'Hello\nworld'); }); }); @@ -1415,10 +1415,10 @@ suite('Editor Controller - Regression tests', () => { editor.setSelection(new Selection(1, 1, 1, 2)); viewModel.type('%', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\'', 'assert2'); }); model.dispose(); @@ -1433,50 +1433,50 @@ suite('Editor Controller - Regression tests', () => { viewModel.type(' ', 'keyboard'); viewModel.type('world', 'keyboard'); viewModel.type(' ', 'keyboard'); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Position(1, 13)); moveLeft(editor, viewModel); moveRight(editor, viewModel); model.pushEditOperations([], [EditOperation.replaceMove(new Range(1, 12, 1, 13), '')], () => []); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Selection(1, 12, 1, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertCursor(viewModel, new Position(1, 1)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world '); + assert.strictEqual(model.getLineContent(1), 'Hello world '); assertCursor(viewModel, new Position(1, 13)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'Hello world'); + assert.strictEqual(model.getLineContent(1), 'Hello world'); assertCursor(viewModel, new Position(1, 12)); }); @@ -1498,7 +1498,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' function baz() {'); + assert.strictEqual(model.getLineContent(1), ' function baz() {'); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1518,7 +1518,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1540,7 +1540,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(1, 9, 1, 9)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(1), ' '); assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); @@ -1568,7 +1568,7 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(7, 1, 7, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(7), '\t'); + assert.strictEqual(model.getLineContent(7), '\t'); assertCursor(viewModel, new Selection(7, 2, 7, 2)); }); @@ -1588,8 +1588,8 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'asdasd'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'asdasd'); }); @@ -1604,12 +1604,12 @@ suite('Editor Controller - Regression tests', () => { assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'asdasd'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'asdasd'); viewModel.cut('keyboard'); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), ''); }); }); @@ -1651,8 +1651,8 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assertCursor(viewModel, new Selection(1, 14, 1, 14)); - assert.equal(model.getLineCount(), 1); - assert.equal(model.getLineContent(1), 'function baz(;'); + assert.strictEqual(model.getLineCount(), 1); + assert.strictEqual(model.getLineContent(1), 'function baz(;'); }); model.dispose(); @@ -1671,9 +1671,9 @@ suite('Editor Controller - Regression tests', () => { viewModel.paste('line1\n', true); - assert.equal(model.getLineContent(1), 'line1'); - assert.equal(model.getLineContent(2), 'line1'); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(1), 'line1'); + assert.strictEqual(model.getLineContent(2), 'line1'); + assert.strictEqual(model.getLineContent(3), ''); }); }); @@ -1689,10 +1689,10 @@ suite('Editor Controller - Regression tests', () => { viewModel.paste('line1\n', true); - assert.equal(model.getLineContent(1), 'line1'); - assert.equal(model.getLineContent(2), 'line line1'); - assert.equal(model.getLineContent(3), ' 2'); - assert.equal(model.getLineContent(4), 'line3'); + assert.strictEqual(model.getLineContent(1), 'line1'); + assert.strictEqual(model.getLineContent(2), 'line line1'); + assert.strictEqual(model.getLineContent(3), ' 2'); + assert.strictEqual(model.getLineContent(4), 'line3'); }); }); @@ -1715,7 +1715,7 @@ suite('Editor Controller - Regression tests', () => { ] ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'a', 'bline1', 'c', @@ -1747,7 +1747,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aaa', 'bbb', 'ccc', @@ -1790,7 +1790,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aaa', 'bbb', 'ccc', @@ -1815,7 +1815,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aline1', 'bline2', 'cline3' @@ -1839,7 +1839,7 @@ suite('Editor Controller - Regression tests', () => { null ); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'aline1', 'bline2', 'cline3' @@ -1869,26 +1869,26 @@ suite('Editor Controller - Regression tests', () => { }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ '\t just some text' ].join('\n'), '001'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' some lines', ' and more lines', ' just some text', ].join('\n'), '002'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', 'just some text', ].join('\n'), '003'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', 'just some text', @@ -1911,7 +1911,7 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('😍', 'keyboard'); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'some lines', 'and more lines', '😍just some text', @@ -1933,7 +1933,7 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { moveTo(editor, viewModel, 3, 2, false); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(3), '\t \tx: 3'); + assert.strictEqual(model.getLineContent(3), '\t \tx: 3'); }); model.dispose(); @@ -1954,7 +1954,7 @@ suite('Editor Controller - Regression tests', () => { moveTo(editor, viewModel, 1, 15, false); moveTo(editor, viewModel, 1, 22, true); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); + assert.strictEqual(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); }); model.dispose(); @@ -1982,8 +1982,8 @@ suite('Editor Controller - Regression tests', () => { CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, args); } - assert.equal(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); - assert.equal(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); + assert.strictEqual(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); + assert.strictEqual(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); } assertWordRight(1, ' '.length + 1); @@ -2048,10 +2048,10 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); }); model.dispose(); @@ -2066,7 +2066,7 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 5) }); - assert.deepEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); }); model.dispose(); @@ -2090,11 +2090,11 @@ suite('Editor Controller - Regression tests', () => { viewModel.replacePreviousChar('せんせい', 4); viewModel.replacePreviousChar('せんせい', 4); - assert.equal(model.getLineContent(1), 'せんせい'); + assert.strictEqual(model.getLineContent(1), 'せんせい'); assertCursor(viewModel, new Position(1, 5)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ''); + assert.strictEqual(model.getLineContent(1), ''); assertCursor(viewModel, new Position(1, 1)); }); }); @@ -2121,11 +2121,11 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('n', 'keyboard'); for (let i = 0; i < LINE_CNT; i++) { - assert.equal(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); + assert.strictEqual(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); } - assert.equal(viewModel.getSelections().length, LINE_CNT); - assert.equal(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); + assert.strictEqual(viewModel.getSelections().length, LINE_CNT); + assert.strictEqual(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); }); }); @@ -2208,6 +2208,42 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #110376: multiple selections with wordwrap behave differently', () => { + // a single model line => 4 view lines + withTestCodeEditor([ + [ + 'just a sentence. just a ', + 'sentence. just a sentence.', + ].join('') + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 25 }, (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 1, 1, 16), + new Selection(1, 18, 1, 33), + new Selection(1, 35, 1, 50), + ]); + + moveLeft(editor, viewModel); + assertCursor(viewModel, [ + new Selection(1, 1, 1, 1), + new Selection(1, 18, 1, 18), + new Selection(1, 35, 1, 35), + ]); + + viewModel.setSelections('test', [ + new Selection(1, 1, 1, 16), + new Selection(1, 18, 1, 33), + new Selection(1, 35, 1, 50), + ]); + + moveRight(editor, viewModel); + assertCursor(viewModel, [ + new Selection(1, 16, 1, 16), + new Selection(1, 33, 1, 33), + new Selection(1, 50, 1, 50), + ]); + }); + }); + test('issue #98320: Multi-Cursor, Wrap lines and cursorSelectRight ==> cursors out of sync', () => { // a single model line => 4 view lines withTestCodeEditor([ @@ -2313,6 +2349,37 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #112301: new stickyTabStops feature interferes with word wrap', () => { + withTestCodeEditor([ + [ + 'function hello() {', + ' console.log(`this is a long console message`)', + '}', + ].join('\n') + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 32, stickyTabStops: true }, (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(2, 31, 2, 31) + ]); + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 32)); + + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 33)); + + moveRight(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 34)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 33)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 32)); + + moveLeft(editor, viewModel, false); + assertCursor(viewModel, new Position(2, 31)); + }); + }); + test('issue #44805: Should not be able to undo in readonly editor', () => { let model = createTextModel( [ @@ -2325,10 +2392,10 @@ suite('Editor Controller - Regression tests', () => { range: new Range(1, 1, 1, 1), text: 'Hello world!' }], () => [new Selection(1, 1, 1, 1)]); - assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'Hello world!'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'Hello world!'); }); model.dispose(); @@ -2339,7 +2406,7 @@ suite('Editor Controller - Regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { return new TokenizationResult2(new Uint32Array(0), state); } }; @@ -2390,8 +2457,8 @@ suite('Editor Controller - Regression tests', () => { viewModel.type('\'', 'keyboard'); - assert.equal(model.getLineContent(1), 'const a = \'foo\';'); - assert.equal(model.getLineContent(2), 'const b = \'\''); + assert.strictEqual(model.getLineContent(1), 'const a = \'foo\';'); + assert.strictEqual(model.getLineContent(2), 'const b = \'\''); }); model.dispose(); @@ -2482,7 +2549,7 @@ suite('Editor Controller - Regression tests', () => { new Selection(2, 1, 2, 1) ]); viewModel.paste('something\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ 'abc123', 'something', '' @@ -2506,22 +2573,22 @@ suite('Editor Controller - Regression tests', () => { ]); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัสด'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัสด'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัส'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัส'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สวั'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวั'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'สว'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สว'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'ส'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ส'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), ''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), ''); }); model.dispose(); @@ -2542,8 +2609,8 @@ suite('Editor Controller - Cursor Configuration', () => { }, (editor, model, viewModel) => { CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(1, 21), source: 'keyboard' }); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' \tMy First Line\t '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' \tMy First Line\t '); + assert.strictEqual(model.getLineContent(2), ' '); }); }); @@ -2566,56 +2633,56 @@ suite('Editor Controller - Cursor Configuration', () => { // Tab on column 1 CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 1) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' My Second Line123'); + assert.strictEqual(model.getLineContent(2), ' My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 2 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 2) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'M y Second Line123'); + assert.strictEqual(model.getLineContent(2), 'M y Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 3 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 3) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 4 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 4) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My S econd Line123'); + assert.strictEqual(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My S econd Line123'); + assert.strictEqual(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 13 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 13) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Li ne123'); + assert.strictEqual(model.getLineContent(2), 'My Second Li ne123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 14 - assert.equal(model.getLineContent(2), 'My Second Line123'); + assert.strictEqual(model.getLineContent(2), 'My Second Line123'); CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 14) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'My Second Lin e123'); + assert.strictEqual(model.getLineContent(2), 'My Second Lin e123'); }); model.dispose(); @@ -2633,7 +2700,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); }); @@ -2650,7 +2717,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); }); @@ -2667,7 +2734,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Selection(1, 7, 1, 7)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); + assert.strictEqual(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); }); mode.dispose(); }); @@ -2685,14 +2752,14 @@ suite('Editor Controller - Cursor Configuration', () => { // Move cursor to the end, verify that we do not trim whitespaces if line has values moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); }); }); @@ -2704,16 +2771,47 @@ suite('Editor Controller - Cursor Configuration', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' '); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); }); }); + test('issue #115033: indent and appendText', () => { + const mode = new class extends MockMode { + constructor() { + super(new LanguageIdentifier('onEnterMode', 3)); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + onEnterRules: [{ + beforeText: /.*/, + action: { + indentAction: IndentAction.Indent, + appendText: 'x' + } + }] + })); + } + }(); + usingCursor({ + text: [ + 'text' + ], + languageIdentifier: mode.getLanguageIdentifier(), + }, (editor, model, viewModel) => { + + moveTo(editor, viewModel, 1, 5); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getLineContent(1), 'text'); + assert.strictEqual(model.getLineContent(2), ' x'); + assertCursor(viewModel, new Position(2, 6)); + }); + mode.dispose(); + }); + test('issue #6862: Editor removes auto inserted indentation when formatting on type', () => { let mode = new OnEnterMode(IndentAction.IndentOutdent); usingCursor({ @@ -2725,9 +2823,9 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 1, 32); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), 'function foo (params: string) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(1), 'function foo (params: string) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), '}'); class TestCommand implements ICommand { @@ -2745,9 +2843,9 @@ suite('Editor Controller - Cursor Configuration', () => { } viewModel.executeCommand(new TestCommand(), 'autoFormat'); - assert.equal(model.getLineContent(1), 'function foo(params: string) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(1), 'function foo(params: string) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), '}'); }); mode.dispose(); }); @@ -2767,27 +2865,27 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }'); moveTo(editor, viewModel, 4, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ' '); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(5), ' }'); moveTo(editor, viewModel, 5, model.getLineMaxColumn(5)); viewModel.type('something', 'keyboard'); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }something'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }something'); }); model.dispose(); @@ -2805,46 +2903,46 @@ suite('Editor Controller - Cursor Configuration', () => { // Move cursor to the end, verify that we do not trim whitespaces if line has values moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); // More whitespaces CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ' '); // Enter and verify that trimmed again viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' some line abc '); - assert.equal(model.getLineContent(2), ''); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ' '); + assert.strictEqual(model.getLineContent(1), ' some line abc '); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ' '); // Trimmed if we will keep only text moveTo(editor, viewModel, 1, 5); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' some line abc '); - assert.equal(model.getLineContent(3), ''); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' some line abc '); + assert.strictEqual(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ''); // Trimmed if we will keep only text by selection moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 3, 1, true); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(1), ' '); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ''); + assert.strictEqual(model.getLineContent(1), ' '); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ''); }); model.dispose(); @@ -2865,7 +2963,7 @@ suite('Editor Controller - Cursor Configuration', () => { moveTo(editor, viewModel, 3, model.getLineMaxColumn(3)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' return 3;', @@ -2875,7 +2973,7 @@ suite('Editor Controller - Cursor Configuration', () => { assertCursor(viewModel, new Position(4, model.getLineMaxColumn(4))); viewModel.paste(' // I\'m gonna copy this line\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' return 3;', @@ -2905,7 +3003,7 @@ suite('Editor Controller - Cursor Configuration', () => { editor.setSelections([new Selection(4, 10, 4, 10)]); viewModel.paste(' // I\'m gonna copy this line\n', true); - assert.equal(model.getValue(), [ + assert.strictEqual(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', ' // Another line', @@ -2932,7 +3030,7 @@ suite('Editor Controller - Cursor Configuration', () => { // DeleteLeft removes just one whitespace moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); }); model.dispose(); @@ -2951,54 +3049,54 @@ suite('Editor Controller - Cursor Configuration', () => { // DeleteLeft does not remove tab size, because some text exists before moveTo(editor, viewModel, 2, model.getLineContent(2).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'a '); + assert.strictEqual(model.getLineContent(2), 'a '); // Undo DeleteLeft - get us back to original indentation CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); // Nothing is broken when cursor is in (1,1) moveTo(editor, viewModel, 1, 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \t x'); + assert.strictEqual(model.getLineContent(1), ' \t \t x'); // DeleteLeft stops at tab stops even in mixed whitespace case moveTo(editor, viewModel, 1, 10); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \t x'); + assert.strictEqual(model.getLineContent(1), ' \t \t x'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \t \tx'); + assert.strictEqual(model.getLineContent(1), ' \t \tx'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' \tx'); + assert.strictEqual(model.getLineContent(1), ' \tx'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'x'); + assert.strictEqual(model.getLineContent(1), 'x'); // DeleteLeft on last line moveTo(editor, viewModel, 3, model.getLineContent(3).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(3), ''); + assert.strictEqual(model.getLineContent(3), ''); // DeleteLeft with removing new line symbol CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x\n a '); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x\n a '); // In case of selection DeleteLeft only deletes selected text moveTo(editor, viewModel, 2, 3); moveTo(editor, viewModel, 2, 4, true); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' a '); + assert.strictEqual(model.getLineContent(2), ' a '); }); model.dispose(); @@ -3016,55 +3114,55 @@ suite('Editor Controller - Cursor Configuration', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); viewModel.type('y', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); viewModel.type('x'); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); }); model.dispose(); @@ -3088,8 +3186,8 @@ suite('Editor Controller - Cursor Configuration', () => { const afterVersion = model.getVersionId(); const afterAltVersion = model.getAlternativeVersionId(); - assert.notEqual(beforeVersion, afterVersion); - assert.equal(beforeAltVersion, afterAltVersion); + assert.notStrictEqual(beforeVersion, afterVersion); + assert.strictEqual(beforeAltVersion, afterAltVersion); }); model.dispose(); @@ -3145,7 +3243,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('}', 'keyboard'); assertCursor(viewModel, new Selection(2, 2, 2, 2)); - assert.equal(model.getLineContent(2), '}', '001'); + assert.strictEqual(model.getLineContent(2), '}', '001'); }); }); @@ -3238,7 +3336,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 1, 4, 1)); - assert.equal(model.getLineContent(3), 'return true;', '001'); + assert.strictEqual(model.getLineContent(3), 'return true;', '001'); }); }); @@ -3259,7 +3357,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(4), '\t}', '001'); + assert.strictEqual(model.getLineContent(4), '\t}', '001'); }); }); @@ -3327,7 +3425,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 16, 3, 16)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), ' if (true) {'); + assert.strictEqual(model.getLineContent(3), ' if (true) {'); assertCursor(viewModel, new Selection(4, 9, 4, 9)); }); }); @@ -3352,7 +3450,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 16, 3, 16)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), ' if (true) {'); + assert.strictEqual(model.getLineContent(3), ' if (true) {'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); }); }); @@ -3375,7 +3473,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(5, 4, 5, 4)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(5), '\t\t}'); + assert.strictEqual(model.getLineContent(5), '\t\t}'); assertCursor(viewModel, new Selection(6, 3, 6, 3)); }); }); @@ -3396,7 +3494,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\t true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\t true;', '001'); }); }); @@ -3416,7 +3514,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\treturn true;', '001'); }); }); @@ -3435,7 +3533,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 5, 4, 5)); - assert.equal(model.getLineContent(4), ' true;', '001'); + assert.strictEqual(model.getLineContent(4), ' true;', '001'); }); }); @@ -3455,14 +3553,14 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\treturn true;', '001'); moveTo(editor, viewModel, 4, 1, false); assertCursor(viewModel, new Selection(4, 1, 4, 1)); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(5), '\t\treturn true;', '002'); + assert.strictEqual(model.getLineContent(5), '\t\treturn true;', '002'); }); }); @@ -3482,14 +3580,14 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), '\t\t\treturn true;', '001'); + assert.strictEqual(model.getLineContent(4), '\t\t\treturn true;', '001'); moveTo(editor, viewModel, 4, 1, false); assertCursor(viewModel, new Selection(4, 1, 4, 1)); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 1, 5, 1)); - assert.equal(model.getLineContent(5), '\t\t\treturn true;', '002'); + assert.strictEqual(model.getLineContent(5), '\t\t\treturn true;', '002'); }); }); @@ -3508,12 +3606,12 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); moveTo(editor, viewModel, 4, 3, false); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(5, 3, 5, 3)); - assert.equal(model.getLineContent(5), ' return true;', '002'); + assert.strictEqual(model.getLineContent(5), ' return true;', '002'); }); }); @@ -3541,12 +3639,12 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 4, 4, 4)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); moveTo(editor, viewModel, 9, 4, false); viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(10, 5, 10, 5)); - assert.equal(model.getLineContent(10), ' return true;', '001'); + assert.strictEqual(model.getLineContent(10), ' return true;', '001'); }); }); @@ -3568,7 +3666,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 3, 4, 3)); - assert.equal(model.getLineContent(4), ' return true;', '001'); + assert.strictEqual(model.getLineContent(4), ' return true;', '001'); }); }); @@ -3590,7 +3688,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 8, 2, 12)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), '\treturn x;'); + assert.strictEqual(model.getLineContent(3), '\treturn x;'); assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3613,7 +3711,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(2, 12, 3, 8)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(3), '\treturn x;'); + assert.strictEqual(model.getLineContent(3), '\treturn x;'); assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3634,9 +3732,9 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(5, 3, 5, 3)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getLineContent(6), '\t'); + assert.strictEqual(model.getLineContent(6), '\t'); assertCursor(viewModel, new Selection(6, 2, 6, 2)); - assert.equal(model.getLineContent(5), '\t}'); + assert.strictEqual(model.getLineContent(5), '\t}'); }); }); @@ -3654,7 +3752,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(4, 2, 4, 2)); - assert.equal(model.getLineContent(4), '\t'); + assert.strictEqual(model.getLineContent(4), '\t'); }); }); @@ -3679,7 +3777,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t'); }); model.dispose(); @@ -3707,7 +3805,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 2, 4, 2)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); model.dispose(); @@ -3735,7 +3833,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t'); }); model.dispose(); @@ -3762,7 +3860,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 3, 4, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t\t'); }); model.dispose(); @@ -3789,7 +3887,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 4, 4, 4)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(4), '\t\t\t\t\t'); + assert.strictEqual(model.getLineContent(4), '\t\t\t\t\t'); }); model.dispose(); @@ -3813,11 +3911,11 @@ suite('Editor Controller - Indentation Rules', () => { moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), ' if (a) {'); - assert.equal(model.getLineContent(2), ' '); - assert.equal(model.getLineContent(3), ' '); - assert.equal(model.getLineContent(4), ''); - assert.equal(model.getLineContent(5), ' }'); + assert.strictEqual(model.getLineContent(1), ' if (a) {'); + assert.strictEqual(model.getLineContent(2), ' '); + assert.strictEqual(model.getLineContent(3), ' '); + assert.strictEqual(model.getLineContent(4), ''); + assert.strictEqual(model.getLineContent(5), ' }'); }); model.dispose(); @@ -3844,7 +3942,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(4, 7, 4, 7)); viewModel.type('d', 'keyboard'); - assert.equal(model.getLineContent(4), ' end'); + assert.strictEqual(model.getLineContent(4), ' end'); }); rubyMode.dispose(); @@ -3867,7 +3965,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('e', 'keyboard'); assertCursor(viewModel, new Selection(5, 4, 5, 4)); - assert.equal(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); + assert.strictEqual(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); }); }); @@ -3888,7 +3986,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type(' ', 'keyboard'); assertCursor(viewModel, new Selection(2, 4, 2, 4)); - assert.equal(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); + assert.strictEqual(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); }); }); @@ -3907,7 +4005,7 @@ suite('Editor Controller - Indentation Rules', () => { viewModel.type('}', 'keyboard'); assertCursor(viewModel, new Selection(3, 2, 3, 2)); - assert.equal(model.getLineContent(3), '}'); + assert.strictEqual(model.getLineContent(3), '}'); }); }); @@ -3954,7 +4052,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(7, 6, 7, 6)); viewModel.type('\n', 'keyboard'); - assert.equal(model.getValue(), + assert.strictEqual(model.getValue(), [ 'class ItemCtrl {', ' getPropertiesByItemId(id) {', @@ -3974,6 +4072,47 @@ suite('Editor Controller - Indentation Rules', () => { mode.dispose(); }); + test('issue #115304: OnEnter broken for TS', () => { + class JSMode extends MockMode { + private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); + constructor() { + super(JSMode._id); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + onEnterRules: javascriptOnEnterRules + })); + } + } + + const mode = new JSMode(); + const model = createTextModel( + [ + '/** */', + 'function f() {}', + ].join('\n'), + undefined, + mode.getLanguageIdentifier() + ); + + withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 4, false); + assertCursor(viewModel, new Selection(1, 4, 1, 4)); + + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), + [ + '/**', + ' * ', + ' */', + 'function f() {}', + ].join('\n') + ); + assertCursor(viewModel, new Selection(2, 4, 2, 4)); + }); + + model.dispose(); + mode.dispose(); + }); + test('issue #38261: TAB key results in bizarre indentation in C++ mode ', () => { class CppMode extends MockMode { private static readonly _id = new LanguageIdentifier('indentRulesMode', 4); @@ -4018,7 +4157,7 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(8, 1, 8, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), + assert.strictEqual(model.getValue(), [ 'int main() {', ' return 0;', @@ -4031,7 +4170,7 @@ suite('Editor Controller - Indentation Rules', () => { ')', ].join('\n') ); - assert.deepEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); + assert.deepStrictEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); }); model.dispose(); @@ -4108,31 +4247,43 @@ suite('Editor Controller - Indentation Rules', () => { assertCursor(viewModel, new Selection(3, 19, 3, 19)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' '); + assert.deepStrictEqual(model.getLineContent(4), ' '); moveTo(editor, viewModel, 5, 18, false); assertCursor(viewModel, new Selection(5, 18, 5, 18)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(6), ' '); + assert.deepStrictEqual(model.getLineContent(6), ' '); moveTo(editor, viewModel, 7, 15, false); assertCursor(viewModel, new Selection(7, 15, 7, 15)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(8), ' '); - assert.deepEqual(model.getLineContent(9), ' ]'); + assert.deepStrictEqual(model.getLineContent(8), ' '); + assert.deepStrictEqual(model.getLineContent(9), ' ]'); moveTo(editor, viewModel, 10, 18, false); assertCursor(viewModel, new Selection(10, 18, 10, 18)); viewModel.type('\n', 'keyboard'); - assert.deepEqual(model.getLineContent(11), ' ]'); + assert.deepStrictEqual(model.getLineContent(11), ' ]'); }); model.dispose(); mode.dispose(); }); + + test('issue #111128: Multicursor `Enter` issue with indentation', () => { + const model = createTextModel(' let a, b, c;', { detectIndentation: false, insertSpaces: false, tabSize: 4 }, mode.getLanguageIdentifier()); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + editor.setSelections([ + new Selection(1, 11, 1, 11), + new Selection(1, 14, 1, 14), + ]); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), ' let a,\n\t b,\n\t c;'); + }); + }); }); interface ICursorOpts { @@ -4182,7 +4333,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '*'); + assert.deepStrictEqual(model.getLineContent(2), '*'); }); mode.dispose(); }); @@ -4198,7 +4349,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4214,7 +4365,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4232,7 +4383,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' } '); + assert.deepStrictEqual(model.getLineContent(4), ' } '); }); mode.dispose(); }); @@ -4250,7 +4401,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 4, 6); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(4), ' } }'); + assert.deepStrictEqual(model.getLineContent(4), ' } }'); }); mode.dispose(); }); @@ -4266,7 +4417,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 1); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }// hello'); + assert.deepStrictEqual(model.getLineContent(2), ' }// hello'); }); mode.dispose(); }); @@ -4282,7 +4433,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' }'); + assert.deepStrictEqual(model.getLineContent(2), ' }'); }); mode.dispose(); }); @@ -4298,7 +4449,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 2); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), 'a}'); + assert.deepStrictEqual(model.getLineContent(2), 'a}'); }); mode.dispose(); }); @@ -4315,7 +4466,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 13); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); + assert.deepStrictEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); }); mode.dispose(); }); @@ -4334,8 +4485,8 @@ suite('ElectricCharacter', () => { changeText = e.changes[0].text; }); viewModel.type(')', 'keyboard'); - assert.deepEqual(model.getLineContent(1), '(div)'); - assert.deepEqual(changeText, ')'); + assert.deepStrictEqual(model.getLineContent(1), '(div)'); + assert.deepStrictEqual(changeText, ')'); }); mode.dispose(); }); @@ -4352,7 +4503,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 3, 3); viewModel.type(')', 'keyboard'); - assert.deepEqual(model.getLineContent(3), '\t3)'); + assert.deepStrictEqual(model.getLineContent(3), '\t3)'); }); mode.dispose(); }); @@ -4368,7 +4519,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 3); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '/** */'); + assert.deepStrictEqual(model.getLineContent(2), '/** */'); }); mode.dispose(); }); @@ -4384,7 +4535,7 @@ suite('ElectricCharacter', () => { }, (editor, model, viewModel) => { moveTo(editor, viewModel, 2, 5); viewModel.type('*', 'keyboard'); - assert.deepEqual(model.getLineContent(2), ' /** */'); + assert.deepStrictEqual(model.getLineContent(2), ' /** */'); }); mode.dispose(); }); @@ -4401,7 +4552,7 @@ suite('ElectricCharacter', () => { moveTo(editor, viewModel, 2, 5); moveTo(editor, viewModel, 2, 1, true); viewModel.type('}', 'keyboard'); - assert.deepEqual(model.getLineContent(2), '}'); + assert.deepStrictEqual(model.getLineContent(2), '}'); }); mode.dispose(); }); @@ -4477,7 +4628,7 @@ suite('autoClosingPairs', () => { let expected = lineContent.substr(0, column - 1) + expectedInsert + lineContent.substr(column - 1); moveTo(editor, viewModel, lineNumber, column); viewModel.type(chr, 'keyboard'); - assert.deepEqual(model.getLineContent(lineNumber), expected, message); + assert.deepStrictEqual(model.getLineContent(lineNumber), expected, message); model.undo(); } @@ -4747,12 +4898,12 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '`var` a = `asd`'); + assert.strictEqual(model.getValue(), '`var` a = `asd`'); // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '`(var)` a = `(asd)`'); + assert.strictEqual(model.getValue(), '`(var)` a = `(asd)`'); }); usingCursor({ @@ -4772,7 +4923,7 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '` a = asd'); + assert.strictEqual(model.getValue(), '` a = asd'); }); usingCursor({ @@ -4791,11 +4942,11 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '`var` a = asd'); + assert.strictEqual(model.getValue(), '`var` a = asd'); // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '`(` a = asd'); + assert.strictEqual(model.getValue(), '`(` a = asd'); }); usingCursor({ @@ -4814,11 +4965,11 @@ suite('autoClosingPairs', () => { // type a ( viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '(var) a = asd'); + assert.strictEqual(model.getValue(), '(var) a = asd'); // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), '(`) a = asd'); + assert.strictEqual(model.getValue(), '(`) a = asd'); }); mode.dispose(); }); @@ -5011,50 +5162,50 @@ suite('autoClosingPairs', () => { // First gif model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste1 = teste\' ok'); - assert.equal(model.getLineContent(1), 'teste1 = teste\' ok'); + assert.strictEqual(model.getLineContent(1), 'teste1 = teste\' ok'); viewModel.setSelections('test', [new Selection(1, 1000, 1, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste2 = teste \'ok'); - assert.equal(model.getLineContent(2), 'teste2 = teste \'ok\''); + assert.strictEqual(model.getLineContent(2), 'teste2 = teste \'ok\''); viewModel.setSelections('test', [new Selection(2, 1000, 2, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste3 = teste" ok'); - assert.equal(model.getLineContent(3), 'teste3 = teste" ok'); + assert.strictEqual(model.getLineContent(3), 'teste3 = teste" ok'); viewModel.setSelections('test', [new Selection(3, 1000, 3, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste4 = teste "ok'); - assert.equal(model.getLineContent(4), 'teste4 = teste "ok"'); + assert.strictEqual(model.getLineContent(4), 'teste4 = teste "ok"'); // Second gif viewModel.setSelections('test', [new Selection(4, 1000, 4, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste \''); - assert.equal(model.getLineContent(5), 'teste \'\''); + assert.strictEqual(model.getLineContent(5), 'teste \'\''); viewModel.setSelections('test', [new Selection(5, 1000, 5, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste "'); - assert.equal(model.getLineContent(6), 'teste ""'); + assert.strictEqual(model.getLineContent(6), 'teste ""'); viewModel.setSelections('test', [new Selection(6, 1000, 6, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste\''); - assert.equal(model.getLineContent(7), 'teste\''); + assert.strictEqual(model.getLineContent(7), 'teste\''); viewModel.setSelections('test', [new Selection(7, 1000, 7, 1000)]); typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste"'); - assert.equal(model.getLineContent(8), 'teste"'); + assert.strictEqual(model.getLineContent(8), 'teste"'); }); mode.dispose(); }); @@ -5301,7 +5452,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('è', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), 'è'); + assert.strictEqual(model.getValue(), 'è'); }); mode.dispose(); }); @@ -5323,7 +5474,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'test\''); + assert.strictEqual(model.getValue(), '\'test\''); }); mode.dispose(); }); @@ -5340,16 +5491,16 @@ suite('autoClosingPairs', () => { viewModel.setSelections('test', [new Selection(1, 13, 1, 13)]); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'\');'); + assert.strictEqual(model.getValue(), 'console.log(\'\');'); viewModel.type('it', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\');'); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\\\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\\\');'); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(), 'console.log(\'it\\\'\');'); + assert.strictEqual(model.getValue(), 'console.log(\'it\\\'\');'); }); mode.dispose(); }); @@ -5366,19 +5517,19 @@ suite('autoClosingPairs', () => { viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), '\\'); + assert.strictEqual(model.getValue(), '\\'); viewModel.type('(', 'keyboard'); - assert.equal(model.getValue(), '\\()'); + assert.strictEqual(model.getValue(), '\\()'); viewModel.type('abc', 'keyboard'); - assert.equal(model.getValue(), '\\(abc)'); + assert.strictEqual(model.getValue(), '\\(abc)'); viewModel.type('\\', 'keyboard'); - assert.equal(model.getValue(), '\\(abc\\)'); + assert.strictEqual(model.getValue(), '\\(abc\\)'); viewModel.type(')', 'keyboard'); - assert.equal(model.getValue(), '\\(abc\\)'); + assert.strictEqual(model.getValue(), '\\(abc\\)'); }); mode.dispose(); }); @@ -5403,7 +5554,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('`', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '`hello\nworld'); + assert.strictEqual(model.getValue(), '`hello\nworld'); assertCursor(viewModel, new Selection(1, 2, 2, 2)); }); mode.dispose(); @@ -5426,14 +5577,14 @@ suite('autoClosingPairs', () => { viewModel.type('\'', 'keyboard'); viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'\''); + assert.strictEqual(model.getValue(), '\'\''); // Typing one more ' + space viewModel.startComposition(); viewModel.type('\'', 'keyboard'); viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'\''); + assert.strictEqual(model.getValue(), '\'\''); // Typing ' as a closing tag model.setValue('\'abc'); @@ -5443,7 +5594,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'abc\''); + assert.strictEqual(model.getValue(), '\'abc\''); // quotes before the newly added character are all paired. model.setValue('\'abc\'def '); @@ -5453,7 +5604,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '\'abc\'def \'\''); + assert.strictEqual(model.getValue(), '\'abc\'def \'\''); // No auto closing if there is non-whitespace character after the cursor model.setValue('abc'); @@ -5471,7 +5622,7 @@ suite('autoClosingPairs', () => { viewModel.replacePreviousChar('\'', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), 'abc\''); + assert.strictEqual(model.getValue(), 'abc\''); }); mode.dispose(); }); @@ -5491,7 +5642,7 @@ suite('autoClosingPairs', () => { viewModel.type('a', 'keyboard'); viewModel.replacePreviousChar('', 1, 'keyboard'); viewModel.endComposition('keyboard'); - assert.equal(model.getValue(), '{}'); + assert.strictEqual(model.getValue(), '{}'); }); mode.dispose(); }); @@ -5513,7 +5664,7 @@ suite('autoClosingPairs', () => { // type a ` viewModel.type('`', 'keyboard'); - assert.equal(model.getValue(), 'var a = `asd`'); + assert.strictEqual(model.getValue(), 'var a = `asd`'); }); mode.dispose(); }); @@ -5541,14 +5692,14 @@ suite('autoClosingPairs', () => { new Selection(1, 12, 1, 13) ]); viewModel.type('"', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); editor.setSelections([ new Selection(1, 9, 1, 10), new Selection(1, 12, 1, 13) ]); viewModel.type('\'', 'keyboard'); - assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); }); model.dispose(); @@ -5574,7 +5725,7 @@ suite('autoClosingPairs', () => { // delete left CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'va a = )'); + assert.strictEqual(model.getValue(), 'va a = )'); }); model.dispose(); mode.dispose(); @@ -5621,20 +5772,20 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A fir line'); + assert.strictEqual(model.getLineContent(1), 'A fir line'); assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5650,20 +5801,20 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A firstine'); + assert.strictEqual(model.getLineContent(1), 'A firstine'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5685,19 +5836,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); viewModel.type('Second', 'keyboard'); - assert.equal(model.getLineContent(2), 'Second line'); + assert.strictEqual(model.getLineContent(2), 'Second line'); assertCursor(viewModel, new Selection(2, 7, 2, 7)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5719,7 +5870,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); @@ -5727,15 +5878,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(2), ''); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), ' line'); + assert.strictEqual(model.getLineContent(2), ' line'); assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5754,19 +5905,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); viewModel.type('text', 'keyboard'); - assert.equal(model.getLineContent(2), 'Another text'); + assert.strictEqual(model.getLineContent(2), 'Another text'); assertCursor(viewModel, new Selection(2, 13, 2, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5785,7 +5936,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -5794,15 +5945,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'An'); + assert.strictEqual(model.getLineContent(2), 'An'); assertCursor(viewModel, new Selection(2, 3, 2, 3)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another '); + assert.strictEqual(model.getLineContent(2), 'Another '); assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(2), 'Another line'); + assert.strictEqual(model.getLineContent(2), 'Another line'); assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5818,19 +5969,19 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first and interesting', 'keyboard'); - assert.equal(model.getLineContent(1), 'A first and interesting line'); + assert.strictEqual(model.getLineContent(1), 'A first and interesting line'); assertCursor(viewModel, new Selection(1, 24, 1, 24)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first and line'); + assert.strictEqual(model.getLineContent(1), 'A first and line'); assertCursor(viewModel, new Selection(1, 12, 1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A first line'); + assert.strictEqual(model.getLineContent(1), 'A first line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getLineContent(1), 'A line'); + assert.strictEqual(model.getLineContent(1), 'A line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5846,15 +5997,15 @@ suite('Undo stops', () => { withTestCodeEditor(null, { model: model }, (editor, viewModel) => { viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); viewModel.type('first', 'keyboard'); - assert.equal(model.getValue(), 'A first line\nAnother line'); + assert.strictEqual(model.getValue(), 'A first line\nAnother line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); model.pushEOL(EndOfLineSequence.CRLF); - assert.equal(model.getValue(), 'A first line\r\nAnother line'); + assert.strictEqual(model.getValue(), 'A first line\r\nAnother line'); assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'A line\nAnother line'); + assert.strictEqual(model.getValue(), 'A line\nAnother line'); assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5873,10 +6024,10 @@ suite('Undo stops', () => { new Selection(1, 7, 1, 12), ]); viewModel.type('no', 'keyboard'); - assert.equal(model.getValue(), 'hello no\nhello no'); + assert.strictEqual(model.getValue(), 'hello no\nhello no'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assert.equal(model.getValue(), 'hello world\nhello world'); + assert.strictEqual(model.getValue(), 'hello world\nhello world'); }); }); }); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 7cb092094a..5a870324b1 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -416,6 +416,91 @@ suite('Cursor move command test', () => { }); }); +suite('Cursor move by blankline test', () => { + + const TEXT = [ + ' \tMy First Line\t ', + '\tMy Second Line', + ' Third Line🐶', + '', + '1', + '2', + '3', + '', + ' ', + 'a', + 'b', + ].join('\n'); + + function executeTest(callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { + callback(editor, viewModel); + }); + } + + test('move down should move to start of next blank line', () => { + executeTest((editor, viewModel) => { + moveDownByBlankLine(viewModel, false); + cursorEqual(viewModel, 4, 1); + }); + }); + + test('move up should move to start of previous blank line', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 7, 1); + moveUpByBlankLine(viewModel, false); + cursorEqual(viewModel, 4, 1); + }); + }); + + test('move down should skip over whitespace if already on blank line', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 8, 1); + moveDownByBlankLine(viewModel, false); + cursorEqual(viewModel, 11, 1); + }); + }); + + test('move up should skip over whitespace if already on blank line', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 9, 1); + moveUpByBlankLine(viewModel, false); + cursorEqual(viewModel, 4, 1); + }); + }); + + test('move up should go to first column of first line if not empty', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 2, 1); + moveUpByBlankLine(viewModel, false); + cursorEqual(viewModel, 1, 1); + }); + }); + + test('move down should go to first column of last line if not empty', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 10, 1); + moveDownByBlankLine(viewModel, false); + cursorEqual(viewModel, 11, 1); + }); + }); + + test('select down should select to start of next blank line', () => { + executeTest((editor, viewModel) => { + moveDownByBlankLine(viewModel, true); + selectionEqual(viewModel.getSelection(), 4, 1, 1, 1); + }); + }); + + test('select up should select to start of previous blank line', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 7, 1); + moveUpByBlankLine(viewModel, true); + selectionEqual(viewModel.getSelection(), 4, 1, 7, 1); + }); + }); +}); + // Move command function move(viewModel: ViewModel, args: any) { @@ -454,6 +539,10 @@ function moveUp(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { move(viewModel, { to: CursorMove.RawDirection.Up, by: CursorMove.RawUnit.WrappedLine, value: noOfLines, select: select }); } +function moveUpByBlankLine(viewModel: ViewModel, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.PrevBlankLine, by: CursorMove.RawUnit.WrappedLine, select: select }); +} + function moveUpByModelLine(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { move(viewModel, { to: CursorMove.RawDirection.Up, value: noOfLines, select: select }); } @@ -462,6 +551,10 @@ function moveDown(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) move(viewModel, { to: CursorMove.RawDirection.Down, by: CursorMove.RawUnit.WrappedLine, value: noOfLines, select: select }); } +function moveDownByBlankLine(viewModel: ViewModel, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.NextBlankLine, by: CursorMove.RawUnit.WrappedLine, select: select }); +} + function moveDownByModelLine(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { move(viewModel, { to: CursorMove.RawDirection.Down, value: noOfLines, select: select }); } @@ -484,11 +577,11 @@ function cursorEqual(viewModel: ViewModel, posLineNumber: number, posColumn: num } function positionEqual(position: Position, lineNumber: number, column: number) { - assert.deepEqual(position, new Position(lineNumber, column), 'position equal'); + assert.deepStrictEqual(position, new Position(lineNumber, column), 'position equal'); } function selectionEqual(selection: Selection, posLineNumber: number, posColumn: number, selLineNumber: number, selColumn: number) { - assert.deepEqual({ + assert.deepStrictEqual({ selectionStartLineNumber: selection.selectionStartLineNumber, selectionStartColumn: selection.selectionStartColumn, positionLineNumber: selection.positionLineNumber, diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index d56654fc10..e39815a063 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -84,8 +84,8 @@ suite('TextAreaState', () => { let actual = TextAreaState.readFromTextArea(textArea); assertTextAreaState(actual, 'Hello world!', 1, 12); - assert.equal(actual.value, 'Hello world!'); - assert.equal(actual.selectionStart, 1); + assert.strictEqual(actual.value, 'Hello world!'); + assert.strictEqual(actual.selectionStart, 1); actual = actual.collapseSelection(); assertTextAreaState(actual, 'Hello world!', 12, 12); @@ -102,23 +102,23 @@ suite('TextAreaState', () => { let state = new TextAreaState('Hi world!', 2, 2, null, null); state.writeToTextArea('test', textArea, false); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 9); - assert.equal(textArea._selectionEnd, 9); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 9); + assert.strictEqual(textArea._selectionEnd, 9); state = new TextAreaState('Hi world!', 3, 3, null, null); state.writeToTextArea('test', textArea, false); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 9); - assert.equal(textArea._selectionEnd, 9); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 9); + assert.strictEqual(textArea._selectionEnd, 9); state = new TextAreaState('Hi world!', 0, 2, null, null); state.writeToTextArea('test', textArea, true); - assert.equal(textArea._value, 'Hi world!'); - assert.equal(textArea._selectionStart, 0); - assert.equal(textArea._selectionEnd, 2); + assert.strictEqual(textArea._value, 'Hi world!'); + assert.strictEqual(textArea._selectionStart, 0); + assert.strictEqual(textArea._selectionEnd, 2); textArea.dispose(); }); @@ -134,8 +134,8 @@ suite('TextAreaState', () => { let newState = TextAreaState.readFromTextArea(textArea); let actual = TextAreaState.deduceInput(prevState, newState, couldBeEmojiInput); - assert.equal(actual.text, expected); - assert.equal(actual.replaceCharCnt, expectedCharReplaceCnt); + assert.strictEqual(actual.text, expected); + assert.strictEqual(actual.replaceCharCnt, expectedCharReplaceCnt); textArea.dispose(); } diff --git a/src/vs/editor/test/browser/core/editorState.test.ts b/src/vs/editor/test/browser/core/editorState.test.ts index 7ce5bfd981..c31daa9601 100644 --- a/src/vs/editor/test/browser/core/editorState.test.ts +++ b/src/vs/editor/test/browser/core/editorState.test.ts @@ -29,7 +29,7 @@ suite('Editor Core - Editor State', () => { test('empty editor state should be valid', () => { let result = validate({}, {}); - assert.equal(result, true); + assert.strictEqual(result, true); }); test('different model URIs should be invalid', () => { @@ -38,7 +38,7 @@ suite('Editor Core - Editor State', () => { { model: { uri: URI.parse('http://test2') } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different model versions should be invalid', () => { @@ -47,7 +47,7 @@ suite('Editor Core - Editor State', () => { { model: { version: 2 } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different positions should be invalid', () => { @@ -56,7 +56,7 @@ suite('Editor Core - Editor State', () => { { position: new Position(2, 3) } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different selections should be invalid', () => { @@ -65,7 +65,7 @@ suite('Editor Core - Editor State', () => { { selection: new Selection(5, 2, 3, 4) } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); test('different scroll positions should be invalid', () => { @@ -74,7 +74,7 @@ suite('Editor Core - Editor State', () => { { scroll: { left: 3, top: 2 } } ); - assert.equal(result, false); + assert.strictEqual(result, false); }); diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index 71620db280..7f4faac0c2 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -60,12 +60,12 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit test('register and resolve decoration type', () => { let s = new TestCodeEditorServiceImpl(themeServiceMock); s.registerDecorationType('example', options); - assert.notEqual(s.resolveDecorationOptions('example', false), undefined); + assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); }); test('remove decoration type', () => { let s = new TestCodeEditorServiceImpl(themeServiceMock); s.registerDecorationType('example', options); - assert.notEqual(s.resolveDecorationOptions('example', false), undefined); + assert.notStrictEqual(s.resolveDecorationOptions('example', false), undefined); s.removeDecorationType('example'); assert.throws(() => s.resolveDecorationOptions('example', false)); }); @@ -95,16 +95,16 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit })); const s = new TestCodeEditorServiceImpl(themeService, styleSheet); s.registerDecorationType('example', options); - assert.equal(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); + assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ff0000;border-color:transparent;box-sizing: border-box;}'); themeService.setTheme(new TestColorTheme({ editorBackground: '#EE0000', editorBorder: '#00FFFF' })); - assert.equal(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ee0000;border-color:#00ffff;box-sizing: border-box;}'); + assert.strictEqual(readStyleSheet(styleSheet), '.monaco-editor .ced-example-0 {background-color:#ee0000;border-color:#00ffff;box-sizing: border-box;}'); s.removeDecorationType('example'); - assert.equal(readStyleSheet(styleSheet), ''); + assert.strictEqual(readStyleSheet(styleSheet), ''); }); test('theme overrides', () => { @@ -134,10 +134,10 @@ suite.skip('Decoration Render Options', () => { // {{SQL CARBON EDIT}} skip suit '.vs.monaco-editor .ced-example-1 {color:#FF00FF !important;}', '.monaco-editor .ced-example-1 {color:#ff0000 !important;}' ].join('\n'); - assert.equal(readStyleSheet(styleSheet), expected); + assert.strictEqual(readStyleSheet(styleSheet), expected); s.removeDecorationType('example'); - assert.equal(readStyleSheet(styleSheet), ''); + assert.strictEqual(readStyleSheet(styleSheet), ''); }); test('css properties, gutterIconPaths', () => { diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 8ab927764e..695e599563 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -31,10 +31,10 @@ export interface ITestCodeEditor extends IActiveCodeEditor { registerAndInstantiateContribution(id: string, ctor: new (editor: ICodeEditor, ...services: Services) => T): T; } -class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { +export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#region testing overrides - protected _createConfiguration(options: IEditorConstructionOptions): IConfiguration { + protected _createConfiguration(options: Readonly): IConfiguration { return new TestConfiguration(options); } protected _createView(viewModel: ViewModel): [View, boolean] { @@ -52,6 +52,9 @@ class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { this._contributions[id] = r; return r; } +} + +class TestCodeEditorWithAutoModelDisposal extends TestCodeEditor { public dispose() { super.dispose(); if (this._modelData) { @@ -144,7 +147,7 @@ export function createTestCodeEditor(options: TestCodeEditorCreationOptions): IT contributions: [] }; const editor = instantiationService.createInstance( - TestCodeEditor, + TestCodeEditorWithAutoModelDisposal, new TestEditorDomElement(), options, codeEditorWidgetOptions diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index 7f69067959..61929b7079 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -85,7 +85,7 @@ suite('MinimapCharRenderer', () => { actual[i] = imageData.data[i]; } - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 0x2D, 0x2D, 0x2D, 0xFF, 0xAC, 0xAC, 0xAC, 0xFF, 0xC6, 0xC6, 0xC6, 0xFF, 0xC8, 0xC8, 0xC8, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0xCB, 0xCB, 0xCB, 0xFF, @@ -115,7 +115,7 @@ suite('MinimapCharRenderer', () => { actual[i] = imageData.data[i]; } - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 0xCB, 0xCB, 0xCB, 0xFF, 0x81, 0x81, 0x81, 0xFF, ]); diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index 32f96ea6b4..4f6725bd78 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -36,7 +36,7 @@ function assertState(col: RenderedLinesCollection, state: ILinesCollec actualState.lines.push(col.getLine(lineNumber).id); actualState.pinged.push(col.getLine(lineNumber)._pinged); } - assert.deepEqual(actualState, state); + assert.deepStrictEqual(actualState, state); } suite('RenderedLinesCollection onLinesDeleted', () => { @@ -54,7 +54,7 @@ suite('RenderedLinesCollection onLinesDeleted', () => { if (actualDeleted1) { actualDeleted = actualDeleted1.map(line => line.id); } - assert.deepEqual(actualDeleted, expectedDeleted); + assert.deepStrictEqual(actualDeleted, expectedDeleted); assertState(col, expectedState); } @@ -325,7 +325,7 @@ suite('RenderedLinesCollection onLineChanged', () => { new TestLine('old9') ]); let actualPinged = col.onLinesChanged(changedLineNumber, changedLineNumber); - assert.deepEqual(actualPinged, expectedPinged); + assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } @@ -410,7 +410,7 @@ suite('RenderedLinesCollection onLinesInserted', () => { if (actualDeleted1) { actualDeleted = actualDeleted1.map(line => line.id); } - assert.deepEqual(actualDeleted, expectedDeleted); + assert.deepStrictEqual(actualDeleted, expectedDeleted); assertState(col, expectedState); } @@ -682,7 +682,7 @@ suite('RenderedLinesCollection onTokensChanged', () => { new TestLine('old9') ]); let actualPinged = col.onTokensChanged([{ fromLineNumber: changedFromLineNumber, toLineNumber: changedToLineNumber }]); - assert.deepEqual(actualPinged, expectedPinged); + assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } diff --git a/src/vs/editor/test/common/config/commonEditorConfig.test.ts b/src/vs/editor/test/common/config/commonEditorConfig.test.ts index 9032ca2029..5cbcabf3dd 100644 --- a/src/vs/editor/test/common/config/commonEditorConfig.test.ts +++ b/src/vs/editor/test/common/config/commonEditorConfig.test.ts @@ -16,40 +16,40 @@ suite('Common Editor Config', () => { const zoom = EditorZoom; zoom.setZoomLevel(0); - assert.equal(zoom.getZoomLevel(), 0); + assert.strictEqual(zoom.getZoomLevel(), 0); zoom.setZoomLevel(-0); - assert.equal(zoom.getZoomLevel(), 0); + assert.strictEqual(zoom.getZoomLevel(), 0); zoom.setZoomLevel(5); - assert.equal(zoom.getZoomLevel(), 5); + assert.strictEqual(zoom.getZoomLevel(), 5); zoom.setZoomLevel(-1); - assert.equal(zoom.getZoomLevel(), -1); + assert.strictEqual(zoom.getZoomLevel(), -1); zoom.setZoomLevel(9); - assert.equal(zoom.getZoomLevel(), 9); + assert.strictEqual(zoom.getZoomLevel(), 9); zoom.setZoomLevel(-9); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(20); - assert.equal(zoom.getZoomLevel(), 20); + assert.strictEqual(zoom.getZoomLevel(), 20); zoom.setZoomLevel(-10); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(9.1); - assert.equal(zoom.getZoomLevel(), 9.1); + assert.strictEqual(zoom.getZoomLevel(), 9.1); zoom.setZoomLevel(-9.1); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); zoom.setZoomLevel(Infinity); - assert.equal(zoom.getZoomLevel(), 20); + assert.strictEqual(zoom.getZoomLevel(), 20); zoom.setZoomLevel(Number.NEGATIVE_INFINITY); - assert.equal(zoom.getZoomLevel(), -5); + assert.strictEqual(zoom.getZoomLevel(), -5); }); class TestWrappingConfiguration extends TestConfiguration { @@ -69,8 +69,8 @@ suite('Common Editor Config', () => { function assertWrapping(config: TestConfiguration, isViewportWrapping: boolean, wrappingColumn: number): void { const options = config.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); - assert.equal(wrappingInfo.isViewportWrapping, isViewportWrapping); - assert.equal(wrappingInfo.wrappingColumn, wrappingColumn); + assert.strictEqual(wrappingInfo.isViewportWrapping, isViewportWrapping); + assert.strictEqual(wrappingInfo.wrappingColumn, wrappingColumn); } test('wordWrap default', () => { @@ -186,26 +186,26 @@ suite('Common Editor Config', () => { }); let config = new TestConfiguration({ hover: hoverOptions }); - assert.equal(config.options.get(EditorOption.hover).enabled, true); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, true); config.updateOptions({ hover: { enabled: false } }); - assert.equal(config.options.get(EditorOption.hover).enabled, false); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, false); }); test('does not emit event when nothing changes', () => { const config = new TestConfiguration({ glyphMargin: true, roundedSelection: false }); let event: ConfigurationChangedEvent | null = null; config.onDidChange(e => event = e); - assert.equal(config.options.get(EditorOption.glyphMargin), true); + assert.strictEqual(config.options.get(EditorOption.glyphMargin), true); config.updateOptions({ glyphMargin: true }); config.updateOptions({ roundedSelection: false }); - assert.equal(event, null); + assert.strictEqual(event, null); }); test('issue #94931: Unable to open source file', () => { const config = new TestConfiguration({ quickSuggestions: null! }); const actual = >>config.options.get(EditorOption.quickSuggestions); - assert.deepEqual(actual, { + assert.deepStrictEqual(actual, { other: true, comments: false, strings: false @@ -216,7 +216,7 @@ suite('Common Editor Config', () => { const config = new TestConfiguration({ quickSuggestions: null! }); config.updateOptions({ quickSuggestions: { strings: true } }); const actual = >>config.options.get(EditorOption.quickSuggestions); - assert.deepEqual(actual, { + assert.deepStrictEqual(actual, { other: true, comments: false, strings: true diff --git a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts index 6c45af4585..d712497f85 100644 --- a/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveHelper.test.ts @@ -8,41 +8,41 @@ import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; suite('CursorMove', () => { test('nextRenderTabStop', () => { - assert.equal(CursorColumns.nextRenderTabStop(0, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(1, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(2, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(3, 4), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(5, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(6, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(7, 4), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 4), 12); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 4), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 4), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 4), 12); - assert.equal(CursorColumns.nextRenderTabStop(0, 2), 2); - assert.equal(CursorColumns.nextRenderTabStop(1, 2), 2); - assert.equal(CursorColumns.nextRenderTabStop(2, 2), 4); - assert.equal(CursorColumns.nextRenderTabStop(3, 2), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 2), 6); - assert.equal(CursorColumns.nextRenderTabStop(5, 2), 6); - assert.equal(CursorColumns.nextRenderTabStop(6, 2), 8); - assert.equal(CursorColumns.nextRenderTabStop(7, 2), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 2), 10); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 2), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 2), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 2), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 2), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 2), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 2), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 2), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 2), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 2), 10); - assert.equal(CursorColumns.nextRenderTabStop(0, 1), 1); - assert.equal(CursorColumns.nextRenderTabStop(1, 1), 2); - assert.equal(CursorColumns.nextRenderTabStop(2, 1), 3); - assert.equal(CursorColumns.nextRenderTabStop(3, 1), 4); - assert.equal(CursorColumns.nextRenderTabStop(4, 1), 5); - assert.equal(CursorColumns.nextRenderTabStop(5, 1), 6); - assert.equal(CursorColumns.nextRenderTabStop(6, 1), 7); - assert.equal(CursorColumns.nextRenderTabStop(7, 1), 8); - assert.equal(CursorColumns.nextRenderTabStop(8, 1), 9); + assert.strictEqual(CursorColumns.nextRenderTabStop(0, 1), 1); + assert.strictEqual(CursorColumns.nextRenderTabStop(1, 1), 2); + assert.strictEqual(CursorColumns.nextRenderTabStop(2, 1), 3); + assert.strictEqual(CursorColumns.nextRenderTabStop(3, 1), 4); + assert.strictEqual(CursorColumns.nextRenderTabStop(4, 1), 5); + assert.strictEqual(CursorColumns.nextRenderTabStop(5, 1), 6); + assert.strictEqual(CursorColumns.nextRenderTabStop(6, 1), 7); + assert.strictEqual(CursorColumns.nextRenderTabStop(7, 1), 8); + assert.strictEqual(CursorColumns.nextRenderTabStop(8, 1), 9); }); test('visibleColumnFromColumn', () => { function testVisibleColumnFromColumn(text: string, tabSize: number, column: number, expected: number): void { - assert.equal(CursorColumns.visibleColumnFromColumn(text, column, tabSize), expected); + assert.strictEqual(CursorColumns.visibleColumnFromColumn(text, column, tabSize), expected); } testVisibleColumnFromColumn('\t\tvar x = 3;', 4, 1, 0); @@ -101,7 +101,7 @@ suite('CursorMove', () => { test('columnFromVisibleColumn', () => { function testColumnFromVisibleColumn(text: string, tabSize: number, visibleColumn: number, expected: number): void { - assert.equal(CursorColumns.columnFromVisibleColumn(text, visibleColumn, tabSize), expected); + assert.strictEqual(CursorColumns.columnFromVisibleColumn(text, visibleColumn, tabSize), expected); } // testColumnFromVisibleColumn('\t\tvar x = 3;', 4, 0, 1); @@ -177,7 +177,7 @@ suite('CursorMove', () => { test('toStatusbarColumn', () => { function t(text: string, tabSize: number, column: number, expected: number): void { - assert.equal(CursorColumns.toStatusbarColumn(text, column, tabSize), expected, `<>`); + assert.strictEqual(CursorColumns.toStatusbarColumn(text, column, tabSize), expected, `<>`); } t(' spaces', 4, 1, 1); diff --git a/src/vs/editor/test/common/core/characterClassifier.test.ts b/src/vs/editor/test/common/core/characterClassifier.test.ts index 2893ea1908..056257d446 100644 --- a/src/vs/editor/test/common/core/characterClassifier.test.ts +++ b/src/vs/editor/test/common/core/characterClassifier.test.ts @@ -11,27 +11,27 @@ suite('CharacterClassifier', () => { test('works', () => { let classifier = new CharacterClassifier(0); - assert.equal(classifier.get(-1), 0); - assert.equal(classifier.get(0), 0); - assert.equal(classifier.get(CharCode.a), 0); - assert.equal(classifier.get(CharCode.b), 0); - assert.equal(classifier.get(CharCode.z), 0); - assert.equal(classifier.get(255), 0); - assert.equal(classifier.get(1000), 0); - assert.equal(classifier.get(2000), 0); + assert.strictEqual(classifier.get(-1), 0); + assert.strictEqual(classifier.get(0), 0); + assert.strictEqual(classifier.get(CharCode.a), 0); + assert.strictEqual(classifier.get(CharCode.b), 0); + assert.strictEqual(classifier.get(CharCode.z), 0); + assert.strictEqual(classifier.get(255), 0); + assert.strictEqual(classifier.get(1000), 0); + assert.strictEqual(classifier.get(2000), 0); classifier.set(CharCode.a, 1); classifier.set(CharCode.z, 2); classifier.set(1000, 3); - assert.equal(classifier.get(-1), 0); - assert.equal(classifier.get(0), 0); - assert.equal(classifier.get(CharCode.a), 1); - assert.equal(classifier.get(CharCode.b), 0); - assert.equal(classifier.get(CharCode.z), 2); - assert.equal(classifier.get(255), 0); - assert.equal(classifier.get(1000), 3); - assert.equal(classifier.get(2000), 0); + assert.strictEqual(classifier.get(-1), 0); + assert.strictEqual(classifier.get(0), 0); + assert.strictEqual(classifier.get(CharCode.a), 1); + assert.strictEqual(classifier.get(CharCode.b), 0); + assert.strictEqual(classifier.get(CharCode.z), 2); + assert.strictEqual(classifier.get(255), 0); + assert.strictEqual(classifier.get(1000), 3); + assert.strictEqual(classifier.get(2000), 0); }); -}); \ No newline at end of file +}); diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index 47c6b60579..9fbc040d5b 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -45,64 +45,64 @@ suite('LineTokens', () => { test('basics', () => { const lineTokens = createTestLineTokens(); - assert.equal(lineTokens.getLineContent(), 'Hello world, this is a lovely day'); - assert.equal(lineTokens.getLineContent().length, 33); - assert.equal(lineTokens.getCount(), 7); + assert.strictEqual(lineTokens.getLineContent(), 'Hello world, this is a lovely day'); + assert.strictEqual(lineTokens.getLineContent().length, 33); + assert.strictEqual(lineTokens.getCount(), 7); - assert.equal(lineTokens.getStartOffset(0), 0); - assert.equal(lineTokens.getEndOffset(0), 6); - assert.equal(lineTokens.getStartOffset(1), 6); - assert.equal(lineTokens.getEndOffset(1), 13); - assert.equal(lineTokens.getStartOffset(2), 13); - assert.equal(lineTokens.getEndOffset(2), 18); - assert.equal(lineTokens.getStartOffset(3), 18); - assert.equal(lineTokens.getEndOffset(3), 21); - assert.equal(lineTokens.getStartOffset(4), 21); - assert.equal(lineTokens.getEndOffset(4), 23); - assert.equal(lineTokens.getStartOffset(5), 23); - assert.equal(lineTokens.getEndOffset(5), 30); - assert.equal(lineTokens.getStartOffset(6), 30); - assert.equal(lineTokens.getEndOffset(6), 33); + assert.strictEqual(lineTokens.getStartOffset(0), 0); + assert.strictEqual(lineTokens.getEndOffset(0), 6); + assert.strictEqual(lineTokens.getStartOffset(1), 6); + assert.strictEqual(lineTokens.getEndOffset(1), 13); + assert.strictEqual(lineTokens.getStartOffset(2), 13); + assert.strictEqual(lineTokens.getEndOffset(2), 18); + assert.strictEqual(lineTokens.getStartOffset(3), 18); + assert.strictEqual(lineTokens.getEndOffset(3), 21); + assert.strictEqual(lineTokens.getStartOffset(4), 21); + assert.strictEqual(lineTokens.getEndOffset(4), 23); + assert.strictEqual(lineTokens.getStartOffset(5), 23); + assert.strictEqual(lineTokens.getEndOffset(5), 30); + assert.strictEqual(lineTokens.getStartOffset(6), 30); + assert.strictEqual(lineTokens.getEndOffset(6), 33); }); test('findToken', () => { const lineTokens = createTestLineTokens(); - assert.equal(lineTokens.findTokenIndexAtOffset(0), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(1), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(2), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(3), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(4), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(5), 0); - assert.equal(lineTokens.findTokenIndexAtOffset(6), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(7), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(8), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(9), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(10), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(11), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(12), 1); - assert.equal(lineTokens.findTokenIndexAtOffset(13), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(14), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(15), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(16), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(17), 2); - assert.equal(lineTokens.findTokenIndexAtOffset(18), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(19), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(20), 3); - assert.equal(lineTokens.findTokenIndexAtOffset(21), 4); - assert.equal(lineTokens.findTokenIndexAtOffset(22), 4); - assert.equal(lineTokens.findTokenIndexAtOffset(23), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(24), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(25), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(26), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(27), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(28), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(29), 5); - assert.equal(lineTokens.findTokenIndexAtOffset(30), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(31), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(32), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(33), 6); - assert.equal(lineTokens.findTokenIndexAtOffset(34), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(0), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(1), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(2), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(3), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(4), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(5), 0); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(6), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(7), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(8), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(9), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(10), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(11), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(12), 1); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(13), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(14), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(15), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(16), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(17), 2); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(18), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(19), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(20), 3); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(21), 4); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(22), 4); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(23), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(24), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(25), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(26), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(27), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(28), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(29), 5); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(30), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(31), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(32), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(33), 6); + assert.strictEqual(lineTokens.findTokenIndexAtOffset(34), 6); }); interface ITestViewLineToken { @@ -118,7 +118,7 @@ suite('LineTokens', () => { foreground: _actual.getForeground(i) }; } - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('inflate', () => { diff --git a/src/vs/editor/test/common/core/range.test.ts b/src/vs/editor/test/common/core/range.test.ts index 9c7734aff3..60ef534b7f 100644 --- a/src/vs/editor/test/common/core/range.test.ts +++ b/src/vs/editor/test/common/core/range.test.ts @@ -9,47 +9,47 @@ import { Range } from 'vs/editor/common/core/range'; suite('Editor Core - Range', () => { test('empty range', () => { let s = new Range(1, 1, 1, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), true); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), true); }); test('swap start and stop same line', () => { let s = new Range(1, 2, 1, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 2); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 2); + assert.strictEqual(s.isEmpty(), false); }); test('swap start and stop', () => { let s = new Range(2, 1, 1, 2); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 2); - assert.equal(s.endLineNumber, 2); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 2); + assert.strictEqual(s.endLineNumber, 2); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), false); }); test('no swap same line', () => { let s = new Range(1, 1, 1, 2); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 1); - assert.equal(s.endColumn, 2); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 1); + assert.strictEqual(s.endColumn, 2); + assert.strictEqual(s.isEmpty(), false); }); test('no swap', () => { let s = new Range(1, 1, 2, 1); - assert.equal(s.startLineNumber, 1); - assert.equal(s.startColumn, 1); - assert.equal(s.endLineNumber, 2); - assert.equal(s.endColumn, 1); - assert.equal(s.isEmpty(), false); + assert.strictEqual(s.startLineNumber, 1); + assert.strictEqual(s.startColumn, 1); + assert.strictEqual(s.endLineNumber, 2); + assert.strictEqual(s.endColumn, 1); + assert.strictEqual(s.isEmpty(), false); }); test('compareRangesUsingEnds', () => { @@ -93,36 +93,36 @@ suite('Editor Core - Range', () => { }); test('containsPosition', () => { - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(1, 3)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 2)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(2, 3)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(3, 1)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 9)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 10)), true); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(5, 11)), false); - assert.equal(new Range(2, 2, 5, 10).containsPosition(new Position(6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(1, 3)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 2)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(2, 3)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(3, 1)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 9)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 10)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(5, 11)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsPosition(new Position(6, 1)), false); }); test('containsRange', () => { - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(1, 3, 2, 2)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 1, 2, 2)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 11)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(5, 9, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(5, 10, 6, 1)), false); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 10)), true); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(2, 3, 5, 9)), true); - assert.equal(new Range(2, 2, 5, 10).containsRange(new Range(3, 100, 4, 100)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(1, 3, 2, 2)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 1, 2, 2)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 11)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(5, 9, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(5, 10, 6, 1)), false); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 2, 5, 10)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(2, 3, 5, 9)), true); + assert.strictEqual(new Range(2, 2, 5, 10).containsRange(new Range(3, 100, 4, 100)), true); }); test('areIntersecting', () => { - assert.equal(Range.areIntersecting(new Range(2, 2, 3, 2), new Range(4, 2, 5, 2)), false); - assert.equal(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(2, 2, 3, 2)), false); - assert.equal(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(5, 2, 6, 2)), false); - assert.equal(Range.areIntersecting(new Range(5, 2, 6, 2), new Range(4, 2, 5, 2)), false); - assert.equal(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 6)), true); - assert.equal(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 9)), true); - assert.equal(Range.areIntersecting(new Range(2, 4, 2, 9), new Range(2, 2, 2, 7)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 3, 2), new Range(4, 2, 5, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(2, 2, 3, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(4, 2, 5, 2), new Range(5, 2, 6, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(5, 2, 6, 2), new Range(4, 2, 5, 2)), false); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 6)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 2, 2, 7), new Range(2, 4, 2, 9)), true); + assert.strictEqual(Range.areIntersecting(new Range(2, 4, 2, 9), new Range(2, 2, 2, 7)), true); }); }); diff --git a/src/vs/editor/test/common/diff/diffComputer.test.ts b/src/vs/editor/test/common/diff/diffComputer.test.ts index 4e2d98fad2..37b3bb5269 100644 --- a/src/vs/editor/test/common/diff/diffComputer.test.ts +++ b/src/vs/editor/test/common/diff/diffComputer.test.ts @@ -64,7 +64,7 @@ function assertDiff(originalLines: string[], modifiedLines: string[], expectedCh for (let i = 0; i < changes.length; i++) { extracted.push(extractLineChangeRepresentation(changes[i], (i < expectedChanges.length ? expectedChanges[i] : null))); } - assert.deepEqual(extracted, expectedChanges); + assert.deepStrictEqual(extracted, expectedChanges); } function createLineDeletion(startLineNumber: number, endLineNumber: number, modifiedLineNumber: number): ILineChange { @@ -462,6 +462,13 @@ suite('Editor Diff - DiffComputer', () => { assertDiff(original, modified, expected, true, false, true); }); + test('empty diff 5', () => { + let original = ['']; + let modified = ['']; + let expected: ILineChange[] = []; + assertDiff(original, modified, expected, true, false, true); + }); + test('pretty diff 1', () => { let original = [ 'suite(function () {', diff --git a/src/vs/editor/test/common/mocks/testConfiguration.ts b/src/vs/editor/test/common/mocks/testConfiguration.ts index ff4ec81f98..33b31d9196 100644 --- a/src/vs/editor/test/common/mocks/testConfiguration.ts +++ b/src/vs/editor/test/common/mocks/testConfiguration.ts @@ -30,6 +30,7 @@ export class TestConfiguration extends CommonEditorConfiguration { protected readConfiguration(styling: BareFontInfo): FontInfo { return new FontInfo({ zoomLevel: 0, + pixelRatio: 1, fontFamily: 'mockFont', fontWeight: 'normal', fontSize: 14, diff --git a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts index b31bb58868..322d82e985 100644 --- a/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts +++ b/src/vs/editor/test/common/model/benchmark/benchmarkUtils.ts @@ -58,7 +58,7 @@ export class BenchmarkSuite { let timeDiffTotal = 0; for (let j = 0; j < this.iterations; j++) { let factory = benchmark.buildBuffer(builder); - let buffer = factory.create(DefaultEndOfLine.LF); + let buffer = factory.create(DefaultEndOfLine.LF).textBuffer; benchmark.preCycle(buffer); let start = process.hrtime(); benchmark.fn(buffer); diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 14ef175098..4db488d449 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -22,10 +22,10 @@ suite('EditorModel - EditableTextModel.applyEdits updates mightContainRTL', () = let model = createEditableTextModelFromString(original.join('\n')); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.mightContainRTL(), before); + assert.strictEqual(model.mightContainRTL(), before); model.applyEdits(edits); - assert.equal(model.mightContainRTL(), after); + assert.strictEqual(model.mightContainRTL(), after); model.dispose(); } @@ -68,10 +68,10 @@ suite('EditorModel - EditableTextModel.applyEdits updates mightContainNonBasicAS let model = createEditableTextModelFromString(original.join('\n')); model.setEOL(EndOfLineSequence.LF); - assert.equal(model.mightContainNonBasicASCII(), before); + assert.strictEqual(model.mightContainNonBasicASCII(), before); model.applyEdits(edits); - assert.equal(model.mightContainNonBasicASCII(), after); + assert.strictEqual(model.mightContainNonBasicASCII(), after); model.dispose(); } @@ -1043,7 +1043,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { test('issue #1580: Changes in line endings are not correctly reflected in the extension host, leading to invalid offsets sent to external refactoring tools', () => { let model = createEditableTextModelFromString('Hello\nWorld!'); - assert.equal(model.getEOL(), '\n'); + assert.strictEqual(model.getEOL(), '\n'); let mirrorModel2 = new MirrorTextModel(null!, model.getLinesContent(), model.getEOL(), model.getVersionId()); let mirrorModel2PrevVersionId = model.getVersionId(); @@ -1058,8 +1058,8 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { }); let assertMirrorModels = () => { - assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); - assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); + assert.strictEqual(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); + assert.strictEqual(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; model.setEOL(EndOfLineSequence.CRLF); @@ -1077,16 +1077,16 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { { range: new Range(1, 2, 1, 2), text: '"' }, ]); - assert.equal(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '"\'"👁\''); - assert.deepEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); + assert.deepStrictEqual(model.validateRange(new Range(1, 3, 1, 4)), new Range(1, 3, 1, 4)); model.applyEdits([ { range: new Range(1, 1, 1, 2), text: null }, { range: new Range(1, 3, 1, 4), text: null }, ]); - assert.equal(model.getValue(EndOfLinePreference.LF), '\'👁\''); + assert.strictEqual(model.getValue(EndOfLinePreference.LF), '\'👁\''); model.dispose(); }); @@ -1108,7 +1108,7 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { model.applyEdits(undoEdits); - assert.deepEqual(model.getValue(), 'line1\nline2\nline3\n'); + assert.deepStrictEqual(model.getValue(), 'line1\nline2\nline3\n'); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 4dbdd112d4..8f0294bef6 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -111,7 +111,7 @@ suite('IntervalTree', () => { let actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.search(new Interval(op.begin, op.end)); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); return; } @@ -123,7 +123,7 @@ suite('IntervalTree', () => { let actual = this._tree.getAllInOrder().map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.intervals; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } public getExistingNodeId(index: number): number { @@ -500,7 +500,7 @@ suite('IntervalTree', () => { function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { let actualNodes = T.intervalSearch(start, end, 0, false, 0); let actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('cormen 1->2', () => { @@ -559,7 +559,7 @@ suite('IntervalTree', () => { let node = new IntervalNode('', nodeStart, nodeEnd); setNodeStickiness(node, nodeStickiness); nodeAcceptEdit(node, start, end, textLength, forceMoveMarkers); - assert.deepEqual([node.start, node.end], [expectedNodeStart, expectedNodeEnd], msg); + assert.deepStrictEqual([node.start, node.end], [expectedNodeStart, expectedNodeEnd], msg); } test('nodeAcceptEdit', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts index 051939772f..a28d20f4c2 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBuffer.test.ts @@ -33,7 +33,7 @@ suite('PieceTreeTextBuffer._getInverseEdits', () => { function assertInverseEdits(ops: IValidatedEditOperation[], expected: Range[]): void { let actual = PieceTreeTextBuffer._getInverseEditRanges(ops); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('single insert', () => { @@ -282,10 +282,10 @@ suite('PieceTreeTextBuffer._toSingleEditOperation', () => { } function testToSingleEditOperation(original: string[], edits: IValidatedEditOperation[], expected: IValidatedEditOperation): void { - const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF); + const textBuffer = createTextBufferFactory(original.join('\n')).create(DefaultEndOfLine.LF).textBuffer; const actual = textBuffer._toSingleEditOperation(edits); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('one edit op is unchanged', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts index ce3d61413e..d61dfd8a54 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/linesTextBufferBuilder.test.ts @@ -10,11 +10,11 @@ import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; export function testTextBufferFactory(text: string, eol: string, mightContainNonBasicASCII: boolean, mightContainRTL: boolean): void { - const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF); + const textBuffer = createTextBufferFactory(text).create(DefaultEndOfLine.LF).textBuffer; - assert.equal(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); - assert.equal(textBuffer.mightContainRTL(), mightContainRTL); - assert.equal(textBuffer.getEOL(), eol); + assert.strictEqual(textBuffer.mightContainNonBasicASCII(), mightContainNonBasicASCII); + assert.strictEqual(textBuffer.mightContainRTL(), mightContainRTL); + assert.strictEqual(textBuffer.getEOL(), eol); } suite('ModelBuilder', () => { diff --git a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts index 0e2114dcf3..e1b9da5683 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts @@ -6,7 +6,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { splitLines } from 'vs/base/common/strings'; import { Range } from 'vs/editor/common/core/range'; -import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; +import { ValidAnnotatedEditOperation } from 'vs/editor/common/model'; export function getRandomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; @@ -127,25 +127,6 @@ export function generateRandomReplaces(chunks: string[], editCnt: number, search return ops; } -export function createMockText(lineCount: number, minColumn: number, maxColumn: number) { - let fixedEOL = getRandomEOLSequence(); - let lines: string[] = []; - for (let i = 0; i < lineCount; i++) { - if (i !== 0) { - lines.push(fixedEOL); - } - lines.push(getRandomString(minColumn, maxColumn)); - } - return lines.join(''); -} - -export function createMockBuffer(str: string, bufferBuilder: ITextBufferBuilder): ITextBuffer { - bufferBuilder.acceptChunk(str); - let bufferFactory = bufferBuilder.finish(); - let buffer = bufferFactory.create(DefaultEndOfLine.LF); - return buffer; -} - export function generateRandomChunkWithLF(minLength: number, maxLength: number): string { let length = getRandomInt(minLength, maxLength); let r = ''; diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index aaa007435d..fb389edb74 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -39,13 +39,13 @@ function assertLineTokens(__actual: LineTokens, _expected: TestToken[]): void { type: token.getType() }; }; - assert.deepEqual(actual, expected.map(decode)); + assert.deepStrictEqual(actual, expected.map(decode)); } suite('ModelLine - getIndentLevel', () => { function assertIndentLevel(text: string, expected: number, tabSize: number = 4): void { let actual = TextModel.computeIndentLevel(text, tabSize); - assert.equal(actual, expected, text); + assert.strictEqual(actual, expected, text); } test('getIndentLevel', () => { @@ -126,7 +126,7 @@ suite('ModelLinesTokens', () => { for (let lineIndex = 0; lineIndex < expected.length; lineIndex++) { const actualLine = model.getLineContent(lineIndex + 1); const actualTokens = model.getLineTokens(lineIndex + 1); - assert.equal(actualLine, expected[lineIndex].text); + assert.strictEqual(actualLine, expected[lineIndex].text); assertLineTokens(actualTokens, expected[lineIndex].tokens); } } diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index d09be2ab9b..a4fcfa08b3 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -21,14 +21,14 @@ suite('Editor Model - Model Modes 1', () => { let calledFor: string[] = []; function checkAndClear(arr: string[]) { - assert.deepEqual(calledFor, arr); + assert.deepStrictEqual(calledFor, arr); calledFor = []; } const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { calledFor.push(line.charAt(0)); return new TokenizationResult2(new Uint32Array(0), state); } @@ -106,7 +106,7 @@ suite('Editor Model - Model Modes 1', () => { checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '0\n-\n+')]); - assert.equal(thisModel.getLineCount(), 7); + assert.strictEqual(thisModel.getLineCount(), 7); thisModel.forceTokenization(7); checkAndClear(['0', '-', '+']); @@ -174,14 +174,14 @@ suite('Editor Model - Model Modes 2', () => { let calledFor: string[] = []; function checkAndClear(arr: string[]): void { - assert.deepEqual(calledFor, arr); + assert.deepStrictEqual(calledFor, arr); calledFor = []; } const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => new ModelState2(''), tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { calledFor.push(line); (state).prevLineContent = line; return new TokenizationResult2(new Uint32Array(0), state); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 4c3742b7fe..175d8d349c 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -46,50 +46,50 @@ suite('Editor Model - Model', () => { // --------- insert text test('model getValue', () => { - assert.equal(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + assert.strictEqual(thisModel.getValue(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); }); test('model insert empty text', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My First Line'); }); test('model insert text without newline 1', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'foo My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'foo My First Line'); }); test('model insert text without newline 2', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' foo')]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My foo First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My foo First Line'); }); test('model insert text with one newline', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); - assert.equal(thisModel.getLineCount(), 6); - assert.equal(thisModel.getLineContent(1), 'My new line'); - assert.equal(thisModel.getLineContent(2), 'No longer First Line'); + assert.strictEqual(thisModel.getLineCount(), 6); + assert.strictEqual(thisModel.getLineContent(1), 'My new line'); + assert.strictEqual(thisModel.getLineContent(2), 'No longer First Line'); }); test('model insert text with two newlines', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nOne more line in the middle\nNo longer')]); - assert.equal(thisModel.getLineCount(), 7); - assert.equal(thisModel.getLineContent(1), 'My new line'); - assert.equal(thisModel.getLineContent(2), 'One more line in the middle'); - assert.equal(thisModel.getLineContent(3), 'No longer First Line'); + assert.strictEqual(thisModel.getLineCount(), 7); + assert.strictEqual(thisModel.getLineContent(1), 'My new line'); + assert.strictEqual(thisModel.getLineContent(2), 'One more line in the middle'); + assert.strictEqual(thisModel.getLineContent(3), 'No longer First Line'); }); test('model insert text with many newlines', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), '\n\n\n\n')]); - assert.equal(thisModel.getLineCount(), 9); - assert.equal(thisModel.getLineContent(1), 'My'); - assert.equal(thisModel.getLineContent(2), ''); - assert.equal(thisModel.getLineContent(3), ''); - assert.equal(thisModel.getLineContent(4), ''); - assert.equal(thisModel.getLineContent(5), ' First Line'); + assert.strictEqual(thisModel.getLineCount(), 9); + assert.strictEqual(thisModel.getLineContent(1), 'My'); + assert.strictEqual(thisModel.getLineContent(2), ''); + assert.strictEqual(thisModel.getLineContent(3), ''); + assert.strictEqual(thisModel.getLineContent(4), ''); + assert.strictEqual(thisModel.getLineContent(5), ' First Line'); }); @@ -111,7 +111,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'foo My First Line') ], @@ -130,7 +130,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My new line'), new ModelRawLinesInserted(2, 2, ['No longer First Line']), @@ -146,47 +146,47 @@ suite('Editor Model - Model', () => { test('model delete empty text', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 1))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'My First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'My First Line'); }); test('model delete text from one line', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'y First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'y First Line'); }); test('model delete text from one line 2', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'a')]); - assert.equal(thisModel.getLineContent(1), 'aMy First Line'); + assert.strictEqual(thisModel.getLineContent(1), 'aMy First Line'); thisModel.applyEdits([EditOperation.delete(new Range(1, 2, 1, 4))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), 'a First Line'); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), 'a First Line'); }); test('model delete all text from a line', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); - assert.equal(thisModel.getLineCount(), 5); - assert.equal(thisModel.getLineContent(1), ''); + assert.strictEqual(thisModel.getLineCount(), 5); + assert.strictEqual(thisModel.getLineContent(1), ''); }); test('model delete text from two lines', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); - assert.equal(thisModel.getLineCount(), 4); - assert.equal(thisModel.getLineContent(1), 'My Second Line'); + assert.strictEqual(thisModel.getLineCount(), 4); + assert.strictEqual(thisModel.getLineContent(1), 'My Second Line'); }); test('model delete text from many lines', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); - assert.equal(thisModel.getLineCount(), 3); - assert.equal(thisModel.getLineContent(1), 'My Third Line'); + assert.strictEqual(thisModel.getLineCount(), 3); + assert.strictEqual(thisModel.getLineContent(1), 'My Third Line'); }); test('model delete everything', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 5, 2))]); - assert.equal(thisModel.getLineCount(), 1); - assert.equal(thisModel.getLineContent(1), ''); + assert.strictEqual(thisModel.getLineCount(), 1); + assert.strictEqual(thisModel.getLineContent(1), ''); }); // --------- delete text eventing @@ -207,7 +207,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'y First Line'), ], @@ -226,7 +226,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, ''), ], @@ -245,7 +245,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Second Line'), new ModelRawLinesDeleted(2, 2), @@ -265,7 +265,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawLineChanged(1, 'My Third Line'), new ModelRawLinesDeleted(2, 3), @@ -279,31 +279,31 @@ suite('Editor Model - Model', () => { // --------- getValueInRange test('getValueInRange', () => { - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 1)), ''); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M'); - assert.equal(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'); - assert.equal(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 1)), ''); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 2)), 'M'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 2, 1, 3)), 'y'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 1, 14)), 'My First Line'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 1)), 'My First Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'); + assert.strictEqual(thisModel.getValueInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'); }); // --------- getValueLengthInRange test('getValueLengthInRange', () => { - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length); - assert.equal(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 2)), 'My First Line\n\t'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 3)), 'My First Line\n\t\t'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 2, 17)), 'My First Line\n\t\tMy Second Line'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 3, 1)), 'My First Line\n\t\tMy Second Line\n'.length); + assert.strictEqual(thisModel.getValueLengthInRange(new Range(1, 1, 4, 1)), 'My First Line\n\t\tMy Second Line\n Third Line\n'.length); }); // --------- setValue @@ -316,7 +316,7 @@ suite('Editor Model - Model', () => { e = _e; }); thisModel.setValue('new value'); - assert.deepEqual(e, new ModelRawContentChangedEvent( + assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ new ModelRawFlush() ], @@ -332,8 +332,8 @@ suite('Editor Model - Model', () => { { range: new Range(1, 1, 1, 1), text: 'b' }, ], true); - assert.deepEqual(res[0].range, new Range(2, 1, 2, 2)); - assert.deepEqual(res[1].range, new Range(1, 1, 1, 2)); + assert.deepStrictEqual(res[0].range, new Range(2, 1, 2, 2)); + assert.deepStrictEqual(res[1].range, new Range(1, 1, 1, 2)); }); }); @@ -358,17 +358,17 @@ suite('Editor Model - Model Line Separators', () => { }); test('model getValue', () => { - assert.equal(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1'); + assert.strictEqual(thisModel.getValue(), 'My First Line\u2028\t\tMy Second Line\n Third Line\u2028\n1'); }); test('model lines', () => { - assert.equal(thisModel.getLineCount(), 3); + assert.strictEqual(thisModel.getLineCount(), 3); }); test('Bug 13333:Model should line break on lonely CR too', () => { let model = createTextModel('Hello\rWorld!\r\nAnother line'); - assert.equal(model.getLineCount(), 3); - assert.equal(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); + assert.strictEqual(model.getLineCount(), 3); + assert.strictEqual(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); model.dispose(); }); }); @@ -389,7 +389,7 @@ suite('Editor Model - Words', () => { this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, { getInitialState: (): IState => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { const tokensArr: number[] = []; let prevLanguageId: LanguageIdentifier | undefined = undefined; for (let i = 0; i < line.length; i++) { @@ -434,17 +434,17 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel(text.join('\n')); disposables.push(thisModel); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: 'This', startColumn: 1, endColumn: 5 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: 'text', startColumn: 6, endColumn: 10 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 19)), { word: 'some', startColumn: 15, endColumn: 19 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 20)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 21)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 26)), { word: 'words', startColumn: 21, endColumn: 26 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 27)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 28)), null); }); test('getWordAtPosition at embedded language boundaries', () => { @@ -455,13 +455,13 @@ suite('Editor Model - Words', () => { const model = createTextModel('abab', undefined, outerMode.getLanguageIdentifier()); disposables.push(model); - assert.deepEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); - assert.deepEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 2)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 3)), { word: 'ab', startColumn: 1, endColumn: 3 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 4)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 5)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 6)), { word: 'xx', startColumn: 4, endColumn: 6 }); + assert.deepStrictEqual(model.getWordAtPosition(new Position(1, 7)), { word: 'ab', startColumn: 7, endColumn: 9 }); }); test('issue #61296: VS code freezes when editing CSS file with emoji', () => { @@ -480,13 +480,13 @@ suite('Editor Model - Words', () => { const thisModel = createTextModel('.🐷-a-b', undefined, MODE_ID); disposables.push(thisModel); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); - assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 2)), { word: '.', startColumn: 1, endColumn: 2 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 3)), null); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 4)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 5)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 6)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 7)), { word: '-a-b', startColumn: 4, endColumn: 8 }); + assert.deepStrictEqual(thisModel.getWordAtPosition(new Position(1, 8)), { word: '-a-b', startColumn: 4, endColumn: 8 }); }); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 9a761d197a..dda7fe6941 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -28,7 +28,7 @@ function modelHasDecorations(model: TextModel, decorations: ILightWeightDecorati }); } modelDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - assert.deepEqual(modelDecorations, decorations); + assert.deepStrictEqual(modelDecorations, decorations); } function modelHasDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string) { @@ -39,7 +39,7 @@ function modelHasDecoration(model: TextModel, startLineNumber: number, startColu } function modelHasNoDecorations(model: TextModel) { - assert.equal(model.getAllDecorations().length, 0, 'Model has no decoration'); + assert.strictEqual(model.getAllDecorations().length, 0, 'Model has no decoration'); } function addDecoration(model: TextModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string): string { @@ -60,7 +60,7 @@ function lineHasDecorations(model: TextModel, lineNumber: number, decorations: { className: decs[i].options.className }); } - assert.deepEqual(lineDecorations, decorations, 'Line decorations'); + assert.deepStrictEqual(lineDecorations, decorations, 'Line decorations'); } function lineHasNoDecorations(model: TextModel, lineNumber: number) { @@ -122,12 +122,12 @@ suite('Editor Model - Model Decorations', () => { addDecoration(thisModel, 1, 1, 2, 1, 'myType'); let line1Decorations = thisModel.getLineDecorations(1); - assert.equal(line1Decorations.length, 1); - assert.equal(line1Decorations[0].options.className, 'myType'); + assert.strictEqual(line1Decorations.length, 1); + assert.strictEqual(line1Decorations[0].options.className, 'myType'); let line2Decorations = thisModel.getLineDecorations(1); - assert.equal(line2Decorations.length, 1); - assert.equal(line2Decorations[0].options.className, 'myType'); + assert.strictEqual(line2Decorations.length, 1); + assert.strictEqual(line2Decorations[0].options.className, 'myType'); lineHasNoDecorations(thisModel, 3); lineHasNoDecorations(thisModel, 4); @@ -138,16 +138,16 @@ suite('Editor Model - Model Decorations', () => { addDecoration(thisModel, 1, 2, 3, 2, 'myType'); let line1Decorations = thisModel.getLineDecorations(1); - assert.equal(line1Decorations.length, 1); - assert.equal(line1Decorations[0].options.className, 'myType'); + assert.strictEqual(line1Decorations.length, 1); + assert.strictEqual(line1Decorations[0].options.className, 'myType'); let line2Decorations = thisModel.getLineDecorations(1); - assert.equal(line2Decorations.length, 1); - assert.equal(line2Decorations[0].options.className, 'myType'); + assert.strictEqual(line2Decorations.length, 1); + assert.strictEqual(line2Decorations[0].options.className, 'myType'); let line3Decorations = thisModel.getLineDecorations(1); - assert.equal(line3Decorations.length, 1); - assert.equal(line3Decorations[0].options.className, 'myType'); + assert.strictEqual(line3Decorations.length, 1); + assert.strictEqual(line3Decorations[0].options.className, 'myType'); lineHasNoDecorations(thisModel, 4); lineHasNoDecorations(thisModel, 5); @@ -209,7 +209,7 @@ suite('Editor Model - Model Decorations', () => { listenerCalled++; }); addDecoration(thisModel, 1, 2, 3, 2, 'myType'); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event on change', () => { @@ -221,7 +221,7 @@ suite('Editor Model - Model Decorations', () => { thisModel.changeDecorations((changeAccessor) => { changeAccessor.changeDecoration(decId, new Range(1, 1, 1, 2)); }); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event on remove', () => { @@ -233,7 +233,7 @@ suite('Editor Model - Model Decorations', () => { thisModel.changeDecorations((changeAccessor) => { changeAccessor.removeDecoration(decId); }); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations emit event when inserting one line text before it', () => { @@ -245,7 +245,7 @@ suite('Editor Model - Model Decorations', () => { }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'Hallo ')]); - assert.equal(listenerCalled, 1, 'listener called'); + assert.strictEqual(listenerCalled, 1, 'listener called'); }); test('decorations do not emit event on no-op deltaDecorations', () => { @@ -260,7 +260,7 @@ suite('Editor Model - Model Decorations', () => { accessor.deltaDecorations([], []); }); - assert.equal(listenerCalled, 0, 'listener not called'); + assert.strictEqual(listenerCalled, 0, 'listener not called'); }); // --------- editing text & effects on decorations @@ -429,7 +429,7 @@ suite('Decorations and editing', () => { forceMoveMarkers: editForceMoveMarkers }]); const actual = model.getDecorationRange(id); - assert.deepEqual(actual, expectedDecRange, msg); + assert.deepStrictEqual(actual, expectedDecRange, msg); model.dispose(); } @@ -1155,20 +1155,20 @@ suite('deltaDecorations', () => { let initialIds = model.deltaDecorations([], decorations.map(toModelDeltaDecoration)); let actualDecorations = readModelDecorations(model, initialIds); - assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); - assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); + assert.strictEqual(initialIds.length, decorations.length, 'returns expected cnt of ids'); + assert.strictEqual(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); - assert.deepEqual(actualDecorations, decorations); + assert.deepStrictEqual(actualDecorations, decorations); let newIds = model.deltaDecorations(initialIds, newDecorations.map(toModelDeltaDecoration)); let actualNewDecorations = readModelDecorations(model, newIds); - assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); - assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); + assert.strictEqual(newIds.length, newDecorations.length, 'returns expected cnt of ids'); + assert.strictEqual(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); - assert.deepEqual(actualDecorations, decorations); + assert.deepStrictEqual(actualDecorations, decorations); model.dispose(); } @@ -1188,8 +1188,8 @@ suite('deltaDecorations', () => { toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); model.dispose(); }); @@ -1294,7 +1294,7 @@ suite('deltaDecorations', () => { let actualDecoration = model.getDecorationOptions(ids[0]); - assert.deepEqual(actualDecoration!.hoverMessage, { value: 'hello2' }); + assert.deepStrictEqual(actualDecoration!.hoverMessage, { value: 'hello2' }); model.dispose(); }); @@ -1326,16 +1326,16 @@ suite('deltaDecorations', () => { toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); ids = model.deltaDecorations(ids, [ toModelDeltaDecoration(decoration('a', 1, 1, 1, 12)), toModelDeltaDecoration(decoration('b', 2, 1, 2, 13)) ]); - assert.deepEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); - assert.deepEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); + assert.deepStrictEqual(model.getDecorationRange(ids[0]), range(1, 1, 1, 12)); + assert.deepStrictEqual(model.getDecorationRange(ids[1]), range(2, 1, 2, 13)); model.dispose(); }); @@ -1365,7 +1365,7 @@ suite('deltaDecorations', () => { let inRangeClassNames = inRange.map(d => d.options.className); inRangeClassNames.sort(); - assert.deepEqual(inRangeClassNames, ['x1', 'x2', 'x3', 'x4']); + assert.deepStrictEqual(inRangeClassNames, ['x1', 'x2', 'x3', 'x4']); model.dispose(); }); @@ -1383,7 +1383,7 @@ suite('deltaDecorations', () => { forceMoveMarkers: false }]); const actual = model.getDecorationRange(id); - assert.deepEqual(actual, new Range(1, 1, 1, 1)); + assert.deepStrictEqual(actual, new Range(1, 1, 1, 1)); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index e4a9de03f2..59111de276 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -52,19 +52,19 @@ suite('Editor Model - Model Edit Operation', () => { let inverseEditOp = model.applyEdits(editOp, true); - assert.equal(model.getLineCount(), editedLines.length); + assert.strictEqual(model.getLineCount(), editedLines.length); for (let i = 0; i < editedLines.length; i++) { - assert.equal(model.getLineContent(i + 1), editedLines[i]); + assert.strictEqual(model.getLineContent(i + 1), editedLines[i]); } let originalOp = model.applyEdits(inverseEditOp, true); - assert.equal(model.getLineCount(), 5); - assert.equal(model.getLineContent(1), LINE1); - assert.equal(model.getLineContent(2), LINE2); - assert.equal(model.getLineContent(3), LINE3); - assert.equal(model.getLineContent(4), LINE4); - assert.equal(model.getLineContent(5), LINE5); + assert.strictEqual(model.getLineCount(), 5); + assert.strictEqual(model.getLineContent(1), LINE1); + assert.strictEqual(model.getLineContent(2), LINE2); + assert.strictEqual(model.getLineContent(3), LINE3); + assert.strictEqual(model.getLineContent(4), LINE4); + assert.strictEqual(model.getLineContent(5), LINE5); const simplifyEdit = (edit: IIdentifiedSingleEditOperation) => { return { @@ -75,7 +75,7 @@ suite('Editor Model - Model Edit Operation', () => { isAutoWhitespaceEdit: edit.isAutoWhitespaceEdit || false }; }; - assert.deepEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); + assert.deepStrictEqual(originalOp.map(simplifyEdit), editOp.map(simplifyEdit)); } test('Insert inline', () => { diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index 026a28b45d..49054c9f51 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -77,11 +77,11 @@ function trimLineFeed(text: string): string { function testLinesContent(str: string, pieceTable: PieceTreeBase) { let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLinesRawContent(), str); for (let i = 0; i < lines.length; i++) { - assert.equal(pieceTable.getLineContent(i + 1), lines[i]); - assert.equal( + assert.strictEqual(pieceTable.getLineContent(i + 1), lines[i]); + assert.strictEqual( trimLineFeed( pieceTable.getValueInRange( new Range( @@ -136,16 +136,16 @@ function testLineStarts(str: string, pieceTable: PieceTreeBase) { } while (m); for (let i = 0; i < lineStarts.length; i++) { - assert.deepEqual( + assert.deepStrictEqual( pieceTable.getPositionAt(lineStarts[i]), new Position(i + 1, 1) ); - assert.equal(pieceTable.getOffsetAt(i + 1, 1), lineStarts[i]); + assert.strictEqual(pieceTable.getOffsetAt(i + 1, 1), lineStarts[i]); } for (let i = 1; i < lineStarts.length; i++) { let pos = pieceTable.getPositionAt(lineStarts[i] - 1); - assert.equal( + assert.strictEqual( pieceTable.getOffsetAt(pos.lineNumber, pos.column), lineStarts[i] - 1 ); @@ -158,7 +158,7 @@ function createTextBuffer(val: string[], normalizeEOL: boolean = true): PieceTre bufferBuilder.acceptChunk(chunk); } let factory = bufferBuilder.finish(normalizeEOL); - return (factory.create(DefaultEndOfLine.LF)).getPieceTree(); + return (factory.create(DefaultEndOfLine.LF).textBuffer).getPieceTree(); } function assertTreeInvariants(T: PieceTreeBase): void { @@ -219,12 +219,12 @@ suite('inserts and deletes', () => { ]); pieceTable.insert(34, 'This is some more text to insert at offset 34.'); - assert.equal( + assert.strictEqual( pieceTable.getLinesRawContent(), 'This is a document with some text.This is some more text to insert at offset 34.' ); pieceTable.delete(42, 5); - assert.equal( + assert.strictEqual( pieceTable.getLinesRawContent(), 'This is a document with some text.This is more text to insert at offset 34.' ); @@ -235,28 +235,28 @@ suite('inserts and deletes', () => { let pt = createTextBuffer(['']); pt.insert(0, 'AAA'); - assert.equal(pt.getLinesRawContent(), 'AAA'); + assert.strictEqual(pt.getLinesRawContent(), 'AAA'); pt.insert(0, 'BBB'); - assert.equal(pt.getLinesRawContent(), 'BBBAAA'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAAA'); pt.insert(6, 'CCC'); - assert.equal(pt.getLinesRawContent(), 'BBBAAACCC'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAAACCC'); pt.insert(5, 'DDD'); - assert.equal(pt.getLinesRawContent(), 'BBBAADDDACCC'); + assert.strictEqual(pt.getLinesRawContent(), 'BBBAADDDACCC'); assertTreeInvariants(pt); }); test('more deletes', () => { let pt = createTextBuffer(['012345678']); pt.delete(8, 1); - assert.equal(pt.getLinesRawContent(), '01234567'); + assert.strictEqual(pt.getLinesRawContent(), '01234567'); pt.delete(0, 1); - assert.equal(pt.getLinesRawContent(), '1234567'); + assert.strictEqual(pt.getLinesRawContent(), '1234567'); pt.delete(5, 1); - assert.equal(pt.getLinesRawContent(), '123457'); + assert.strictEqual(pt.getLinesRawContent(), '123457'); pt.delete(5, 1); - assert.equal(pt.getLinesRawContent(), '12345'); + assert.strictEqual(pt.getLinesRawContent(), '12345'); pt.delete(0, 5); - assert.equal(pt.getLinesRawContent(), ''); + assert.strictEqual(pt.getLinesRawContent(), ''); assertTreeInvariants(pt); }); @@ -265,17 +265,17 @@ suite('inserts and deletes', () => { let pieceTable = createTextBuffer(['']); pieceTable.insert(0, 'ceLPHmFzvCtFeHkCBej '); str = str.substring(0, 0) + 'ceLPHmFzvCtFeHkCBej ' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(8, 'gDCEfNYiBUNkSwtvB K '); str = str.substring(0, 8) + 'gDCEfNYiBUNkSwtvB K ' + str.substring(8); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(38, 'cyNcHxjNPPoehBJldLS '); str = str.substring(0, 38) + 'cyNcHxjNPPoehBJldLS ' + str.substring(38); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(59, 'ejMx\nOTgWlbpeDExjOk '); str = str.substring(0, 59) + 'ejMx\nOTgWlbpeDExjOk ' + str.substring(59); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -293,7 +293,7 @@ suite('inserts and deletes', () => { pieceTable.insert(10, 'Gbtp '); str = str.substring(0, 10) + 'Gbtp ' + str.substring(10); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -310,7 +310,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 2) + 'GGZB' + str.substring(2); pieceTable.insert(12, 'wXpq'); str = str.substring(0, 12) + 'wXpq' + str.substring(12); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); }); test('random delete 1', () => { @@ -319,30 +319,30 @@ suite('inserts and deletes', () => { pieceTable.insert(0, 'vfb'); str = str.substring(0, 0) + 'vfb' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(0, 'zRq'); str = str.substring(0, 0) + 'zRq' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(5, 1); str = str.substring(0, 5) + str.substring(5 + 1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.insert(1, 'UNw'); str = str.substring(0, 1) + 'UNw' + str.substring(1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(4, 3); str = str.substring(0, 4) + str.substring(4 + 3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(1, 4); str = str.substring(0, 1) + str.substring(1 + 4); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(0, 1); str = str.substring(0, 0) + str.substring(0 + 1); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -368,7 +368,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 6) + str.substring(6 + 7); pieceTable.delete(3, 5); str = str.substring(0, 3) + str.substring(3 + 5); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -401,7 +401,7 @@ suite('inserts and deletes', () => { str = str.substring(0, 5) + str.substring(5 + 8); pieceTable.delete(3, 4); str = str.substring(0, 3) + str.substring(3 + 4); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -427,7 +427,7 @@ suite('inserts and deletes', () => { pieceTable.insert(5, '\n\na\r'); str = str.substring(0, 5) + '\n\na\r' + str.substring(5); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -455,7 +455,7 @@ suite('inserts and deletes', () => { pieceTable.insert(2, 'a\ra\n'); str = str.substring(0, 2) + 'a\ra\n' + str.substring(2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -480,11 +480,11 @@ suite('inserts and deletes', () => { str = str.substring(0, 5) + '\n\na\r' + str.substring(5); pieceTable.insert(10, '\r\r\n\r'); str = str.substring(0, 10) + '\r\r\n\r' + str.substring(10); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); pieceTable.delete(21, 3); str = str.substring(0, 21) + str.substring(21 + 3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); @@ -512,7 +512,7 @@ suite('inserts and deletes', () => { pieceTable.insert(3, 'a\naa'); str = str.substring(0, 3) + 'a\naa' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); test('random insert/delete \\r bug 5', () => { @@ -539,7 +539,7 @@ suite('inserts and deletes', () => { pieceTable.insert(15, '\n\r\r\r'); str = str.substring(0, 15) + '\n\r\r\r' + str.substring(15); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); assertTreeInvariants(pieceTable); }); }); @@ -548,22 +548,22 @@ suite('prefix sum for line feed', () => { test('basic', () => { let pieceTable = createTextBuffer(['1\n2\n3\n4']); - assert.equal(pieceTable.getLineCount(), 4); - assert.deepEqual(pieceTable.getPositionAt(0), new Position(1, 1)); - assert.deepEqual(pieceTable.getPositionAt(1), new Position(1, 2)); - assert.deepEqual(pieceTable.getPositionAt(2), new Position(2, 1)); - assert.deepEqual(pieceTable.getPositionAt(3), new Position(2, 2)); - assert.deepEqual(pieceTable.getPositionAt(4), new Position(3, 1)); - assert.deepEqual(pieceTable.getPositionAt(5), new Position(3, 2)); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.strictEqual(pieceTable.getLineCount(), 4); + assert.deepStrictEqual(pieceTable.getPositionAt(0), new Position(1, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(1), new Position(1, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(2), new Position(2, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(3), new Position(2, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(4), new Position(3, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(5), new Position(3, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.equal(pieceTable.getOffsetAt(1, 1), 0); - assert.equal(pieceTable.getOffsetAt(1, 2), 1); - assert.equal(pieceTable.getOffsetAt(2, 1), 2); - assert.equal(pieceTable.getOffsetAt(2, 2), 3); - assert.equal(pieceTable.getOffsetAt(3, 1), 4); - assert.equal(pieceTable.getOffsetAt(3, 2), 5); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(1, 1), 0); + assert.strictEqual(pieceTable.getOffsetAt(1, 2), 1); + assert.strictEqual(pieceTable.getOffsetAt(2, 1), 2); + assert.strictEqual(pieceTable.getOffsetAt(2, 2), 3); + assert.strictEqual(pieceTable.getOffsetAt(3, 1), 4); + assert.strictEqual(pieceTable.getOffsetAt(3, 2), 5); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); assertTreeInvariants(pieceTable); }); @@ -571,9 +571,9 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\nde']); pieceTable.insert(8, 'fh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(4, 4)); - assert.equal(pieceTable.getOffsetAt(1, 1), 0); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(4, 4)); + assert.strictEqual(pieceTable.getOffsetAt(1, 1), 0); assertTreeInvariants(pieceTable); }); @@ -581,22 +581,22 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\nde']); pieceTable.insert(7, 'fh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(4, 4)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(14), new Position(6, 3)); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(4, 4)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(14), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(4, 4), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 12); - assert.equal(pieceTable.getOffsetAt(6, 2), 13); - assert.equal(pieceTable.getOffsetAt(6, 3), 14); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(4, 4), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 13); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 14); assertTreeInvariants(pieceTable); }); @@ -604,23 +604,23 @@ suite('prefix sum for line feed', () => { let pieceTable = createTextBuffer(['a\nb\nc\ndefh\ni\njk']); pieceTable.delete(7, 2); - assert.equal(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(5, 1)); - assert.deepEqual(pieceTable.getPositionAt(11), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 3)); + assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(5, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(11), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(5, 1), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 11); - assert.equal(pieceTable.getOffsetAt(6, 2), 12); - assert.equal(pieceTable.getOffsetAt(6, 3), 13); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(5, 1), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 11); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 13); assertTreeInvariants(pieceTable); }); @@ -629,23 +629,23 @@ suite('prefix sum for line feed', () => { pieceTable.insert(8, 'fh\ni\njk'); pieceTable.delete(7, 2); - assert.equal(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); - assert.equal(pieceTable.getLineCount(), 6); - assert.deepEqual(pieceTable.getPositionAt(6), new Position(4, 1)); - assert.deepEqual(pieceTable.getPositionAt(7), new Position(4, 2)); - assert.deepEqual(pieceTable.getPositionAt(8), new Position(4, 3)); - assert.deepEqual(pieceTable.getPositionAt(9), new Position(5, 1)); - assert.deepEqual(pieceTable.getPositionAt(11), new Position(6, 1)); - assert.deepEqual(pieceTable.getPositionAt(12), new Position(6, 2)); - assert.deepEqual(pieceTable.getPositionAt(13), new Position(6, 3)); + assert.strictEqual(pieceTable.getLinesRawContent(), 'a\nb\nc\ndh\ni\njk'); + assert.strictEqual(pieceTable.getLineCount(), 6); + assert.deepStrictEqual(pieceTable.getPositionAt(6), new Position(4, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(7), new Position(4, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(8), new Position(4, 3)); + assert.deepStrictEqual(pieceTable.getPositionAt(9), new Position(5, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(11), new Position(6, 1)); + assert.deepStrictEqual(pieceTable.getPositionAt(12), new Position(6, 2)); + assert.deepStrictEqual(pieceTable.getPositionAt(13), new Position(6, 3)); - assert.equal(pieceTable.getOffsetAt(4, 1), 6); - assert.equal(pieceTable.getOffsetAt(4, 2), 7); - assert.equal(pieceTable.getOffsetAt(4, 3), 8); - assert.equal(pieceTable.getOffsetAt(5, 1), 9); - assert.equal(pieceTable.getOffsetAt(6, 1), 11); - assert.equal(pieceTable.getOffsetAt(6, 2), 12); - assert.equal(pieceTable.getOffsetAt(6, 3), 13); + assert.strictEqual(pieceTable.getOffsetAt(4, 1), 6); + assert.strictEqual(pieceTable.getOffsetAt(4, 2), 7); + assert.strictEqual(pieceTable.getOffsetAt(4, 3), 8); + assert.strictEqual(pieceTable.getOffsetAt(5, 1), 9); + assert.strictEqual(pieceTable.getOffsetAt(6, 1), 11); + assert.strictEqual(pieceTable.getOffsetAt(6, 2), 12); + assert.strictEqual(pieceTable.getOffsetAt(6, 3), 13); assertTreeInvariants(pieceTable); }); @@ -661,7 +661,7 @@ suite('prefix sum for line feed', () => { str = str.substring(0, 14) + 'X ZZ\nYZZYZXXY Y XY\n ' + str.substring(14); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -675,7 +675,7 @@ suite('prefix sum for line feed', () => { pieceTable.insert(3, 'XXY \n\nY Y YYY ZYXY '); str = str.substring(0, 3) + 'XXY \n\nY Y YYY ZYXY ' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -803,12 +803,12 @@ suite('get text in range', () => { pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' - assert.equal(pieceTable.getValueInRange(new Range(1, 1, 1, 3)), 'a\n'); - assert.equal(pieceTable.getValueInRange(new Range(2, 1, 2, 3)), 'b\n'); - assert.equal(pieceTable.getValueInRange(new Range(3, 1, 3, 3)), 'c\n'); - assert.equal(pieceTable.getValueInRange(new Range(4, 1, 4, 4)), 'dh\n'); - assert.equal(pieceTable.getValueInRange(new Range(5, 1, 5, 3)), 'i\n'); - assert.equal(pieceTable.getValueInRange(new Range(6, 1, 6, 3)), 'jk'); + assert.strictEqual(pieceTable.getValueInRange(new Range(1, 1, 1, 3)), 'a\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(2, 1, 2, 3)), 'b\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(3, 1, 3, 3)), 'c\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(4, 1, 4, 4)), 'dh\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(5, 1, 5, 3)), 'i\n'); + assert.strictEqual(pieceTable.getValueInRange(new Range(6, 1, 6, 3)), 'jk'); assertTreeInvariants(pieceTable); }); @@ -902,18 +902,18 @@ suite('get text in range', () => { test('get line content', () => { let pieceTable = createTextBuffer(['1']); - assert.equal(pieceTable.getLineRawContent(1), '1'); + assert.strictEqual(pieceTable.getLineRawContent(1), '1'); pieceTable.insert(1, '2'); - assert.equal(pieceTable.getLineRawContent(1), '12'); + assert.strictEqual(pieceTable.getLineRawContent(1), '12'); assertTreeInvariants(pieceTable); }); test('get line content basic', () => { let pieceTable = createTextBuffer(['1\n2\n3\n4']); - assert.equal(pieceTable.getLineRawContent(1), '1\n'); - assert.equal(pieceTable.getLineRawContent(2), '2\n'); - assert.equal(pieceTable.getLineRawContent(3), '3\n'); - assert.equal(pieceTable.getLineRawContent(4), '4'); + assert.strictEqual(pieceTable.getLineRawContent(1), '1\n'); + assert.strictEqual(pieceTable.getLineRawContent(2), '2\n'); + assert.strictEqual(pieceTable.getLineRawContent(3), '3\n'); + assert.strictEqual(pieceTable.getLineRawContent(4), '4'); assertTreeInvariants(pieceTable); }); @@ -923,12 +923,12 @@ suite('get text in range', () => { pieceTable.delete(7, 2); // 'a\nb\nc\ndh\ni\njk' - assert.equal(pieceTable.getLineRawContent(1), 'a\n'); - assert.equal(pieceTable.getLineRawContent(2), 'b\n'); - assert.equal(pieceTable.getLineRawContent(3), 'c\n'); - assert.equal(pieceTable.getLineRawContent(4), 'dh\n'); - assert.equal(pieceTable.getLineRawContent(5), 'i\n'); - assert.equal(pieceTable.getLineRawContent(6), 'jk'); + assert.strictEqual(pieceTable.getLineRawContent(1), 'a\n'); + assert.strictEqual(pieceTable.getLineRawContent(2), 'b\n'); + assert.strictEqual(pieceTable.getLineRawContent(3), 'c\n'); + assert.strictEqual(pieceTable.getLineRawContent(4), 'dh\n'); + assert.strictEqual(pieceTable.getLineRawContent(5), 'i\n'); + assert.strictEqual(pieceTable.getLineRawContent(6), 'jk'); assertTreeInvariants(pieceTable); }); @@ -973,7 +973,7 @@ suite('CRLF', () => { pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(0, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -982,7 +982,7 @@ suite('CRLF', () => { pieceTable.insert(0, 'a\r\nb'); pieceTable.delete(2, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -999,7 +999,7 @@ suite('CRLF', () => { str = str.substring(0, 2) + str.substring(2 + 3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 2', () => { @@ -1014,7 +1014,7 @@ suite('CRLF', () => { str = str.substring(0, 4) + str.substring(4 + 1); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 3', () => { @@ -1035,7 +1035,7 @@ suite('CRLF', () => { str = str.substring(0, 3) + '\r\r\r\n' + str.substring(3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 4', () => { @@ -1185,14 +1185,14 @@ suite('centralized lineStarts with CRLF', () => { test('delete CR in CRLF 1', () => { let pieceTable = createTextBuffer(['a\r\nb'], false); pieceTable.delete(2, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); test('delete CR in CRLF 2', () => { let pieceTable = createTextBuffer(['a\r\nb']); pieceTable.delete(0, 2); - assert.equal(pieceTable.getLineCount(), 2); + assert.strictEqual(pieceTable.getLineCount(), 2); assertTreeInvariants(pieceTable); }); @@ -1207,7 +1207,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 2) + str.substring(2 + 3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); test('random bug 2', () => { @@ -1220,7 +1220,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 4) + str.substring(4 + 1); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); @@ -1240,7 +1240,7 @@ suite('centralized lineStarts with CRLF', () => { str = str.substring(0, 3) + '\r\r\r\n' + str.substring(3); let lines = splitLines(str); - assert.equal(pieceTable.getLineCount(), lines.length); + assert.strictEqual(pieceTable.getLineCount(), lines.length); assertTreeInvariants(pieceTable); }); @@ -1386,7 +1386,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.insert(7, '\r\r\r\r'); str = str.substring(0, 7) + '\r\r\r\r' + str.substring(7); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1407,7 +1407,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.delete(11, 2); str = str.substring(0, 11) + str.substring(11 + 2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1424,7 +1424,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.delete(1, 2); str = str.substring(0, 1) + str.substring(1 + 2); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1437,7 +1437,7 @@ suite('centralized lineStarts with CRLF', () => { pieceTable.insert(3, '\r\n\n\n'); str = str.substring(0, 3) + '\r\n\n\n' + str.substring(3); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); assertTreeInvariants(pieceTable); }); @@ -1469,7 +1469,7 @@ suite('random is unsupervised', () => { pieceTable.insert(0, 'VZXXZYZX\r'); str = str.substring(0, 0) + 'VZXXZYZX\r' + str.substring(0); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); @@ -1507,7 +1507,7 @@ suite('random is unsupervised', () => { } // console.log(output); - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); @@ -1543,7 +1543,7 @@ suite('random is unsupervised', () => { } } - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); assertTreeInvariants(pieceTable); @@ -1577,7 +1577,7 @@ suite('random is unsupervised', () => { testLinesContent(str, pieceTable); } - assert.equal(pieceTable.getLinesRawContent(), str); + assert.strictEqual(pieceTable.getLinesRawContent(), str); testLineStarts(str, pieceTable); testLinesContent(str, pieceTable); assertTreeInvariants(pieceTable); @@ -1612,33 +1612,33 @@ suite('buffer api', () => { test('getLineCharCode - issue #45735', () => { let pieceTable = createTextBuffer(['LINE1\nline2']); - assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); - assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); - assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); - assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); - assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); - assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); - assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); - assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); - assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); - assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); - assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.strictEqual(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.strictEqual(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.strictEqual(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.strictEqual(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.strictEqual(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); test('getLineCharCode - issue #47733', () => { let pieceTable = createTextBuffer(['', 'LINE1\n', 'line2']); - assert.equal(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); - assert.equal(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); - assert.equal(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); - assert.equal(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); - assert.equal(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); - assert.equal(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); - assert.equal(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); - assert.equal(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); - assert.equal(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); - assert.equal(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); - assert.equal(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); + assert.strictEqual(pieceTable.getLineCharCode(1, 0), 'L'.charCodeAt(0), 'L'); + assert.strictEqual(pieceTable.getLineCharCode(1, 1), 'I'.charCodeAt(0), 'I'); + assert.strictEqual(pieceTable.getLineCharCode(1, 2), 'N'.charCodeAt(0), 'N'); + assert.strictEqual(pieceTable.getLineCharCode(1, 3), 'E'.charCodeAt(0), 'E'); + assert.strictEqual(pieceTable.getLineCharCode(1, 4), '1'.charCodeAt(0), '1'); + assert.strictEqual(pieceTable.getLineCharCode(1, 5), '\n'.charCodeAt(0), '\\n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 0), 'l'.charCodeAt(0), 'l'); + assert.strictEqual(pieceTable.getLineCharCode(2, 1), 'i'.charCodeAt(0), 'i'); + assert.strictEqual(pieceTable.getLineCharCode(2, 2), 'n'.charCodeAt(0), 'n'); + assert.strictEqual(pieceTable.getLineCharCode(2, 3), 'e'.charCodeAt(0), 'e'); + assert.strictEqual(pieceTable.getLineCharCode(2, 4), '2'.charCodeAt(0), '2'); }); }); @@ -1771,7 +1771,7 @@ suite('snapshot', () => { ]); const snapshot = model.createSnapshot(); const snapshot1 = model.createSnapshot(); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); model.applyEdits([ { @@ -1786,7 +1786,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot1)); }); test('immutable snapshot 1', () => { @@ -1806,7 +1806,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); test('immutable snapshot 2', () => { @@ -1826,7 +1826,7 @@ suite('snapshot', () => { } ]); - assert.equal(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.strictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); test('immutable snapshot 3', () => { @@ -1845,7 +1845,7 @@ suite('snapshot', () => { } ]); - assert.notEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); + assert.notStrictEqual(model.getLinesContent().join('\n'), getValueInSnapshot(snapshot)); }); }); @@ -1854,7 +1854,7 @@ suite('chunk based search', () => { let pieceTree = createTextBuffer(['']); pieceTree.delete(0, 1); let ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 1, 1), new SearchData(/abc/, new WordCharacterClassifier(',./'), 'abc'), true, 1000); - assert.equal(ret.length, 0); + assert.strictEqual(ret.length, 0); }); test('#45770. FindInNode should not cross node boundary.', () => { @@ -1873,11 +1873,11 @@ suite('chunk based search', () => { pieceTree.insert(16, ' '); let ret = pieceTree.findMatchesLineByLine(new Range(1, 1, 4, 13), new SearchData(/\[/gi, new WordCharacterClassifier(',./'), '['), true, 1000); - assert.equal(ret.length, 3); + assert.strictEqual(ret.length, 3); - assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); - assert.deepEqual(ret[1].range, new Range(3, 3, 3, 4)); - assert.deepEqual(ret[2].range, new Range(4, 3, 4, 4)); + assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); + assert.deepStrictEqual(ret[1].range, new Range(3, 3, 3, 4)); + assert.deepStrictEqual(ret[2].range, new Range(4, 3, 4, 4)); }); test('search searching from the middle', () => { @@ -1889,12 +1889,12 @@ suite('chunk based search', () => { ]); pieceTree.delete(4, 1); let ret = pieceTree.findMatchesLineByLine(new Range(2, 3, 2, 6), new SearchData(/a/gi, null, 'a'), true, 1000); - assert.equal(ret.length, 1); - assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); + assert.strictEqual(ret.length, 1); + assert.deepStrictEqual(ret[0].range, new Range(2, 3, 2, 4)); pieceTree.delete(4, 1); ret = pieceTree.findMatchesLineByLine(new Range(2, 2, 2, 5), new SearchData(/a/gi, null, 'a'), true, 1000); - assert.equal(ret.length, 1); - assert.deepEqual(ret[0].range, new Range(2, 2, 2, 3)); + assert.strictEqual(ret.length, 1); + assert.deepStrictEqual(ret[0].range, new Range(2, 2, 2, 3)); }); }); diff --git a/src/vs/editor/test/common/model/textChange.test.ts b/src/vs/editor/test/common/model/textChange.test.ts index 46f23e6ce0..0d9346adb3 100644 --- a/src/vs/editor/test/common/model/textChange.test.ts +++ b/src/vs/editor/test/common/model/textChange.test.ts @@ -75,7 +75,7 @@ suite('TextChangeCompressor', () => { }; }); let actualDoResult = getResultingContent(initialText, compressedDoTextEdits); - assert.equal(actualDoResult, finalText); + assert.strictEqual(actualDoResult, finalText); let compressedUndoTextEdits: IGeneratedEdit[] = compressedTextChanges.map((change) => { return { @@ -85,7 +85,7 @@ suite('TextChangeCompressor', () => { }; }); let actualUndoResult = getResultingContent(finalText, compressedUndoTextEdits); - assert.equal(actualUndoResult, initialText); + assert.strictEqual(actualUndoResult, initialText); } test('simple 1', () => { diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 4b8995aa31..d666e63172 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -22,8 +22,8 @@ function testGuessIndentation(defaultInsertSpaces: boolean, defaultTabSize: numb let r = m.getOptions(); m.dispose(); - assert.equal(r.insertSpaces, expectedInsertSpaces, msg); - assert.equal(r.tabSize, expectedTabSize, msg); + assert.strictEqual(r.insertSpaces, expectedInsertSpaces, msg); + assert.strictEqual(r.tabSize, expectedTabSize, msg); } function assertGuess(expectedInsertSpaces: boolean | undefined, expectedTabSize: number | undefined | [number], text: string[], msg?: string): void { @@ -75,14 +75,14 @@ suite('TextModelData.fromString', () => { } function testTextModelDataFromString(text: string, expected: ITextBufferData): void { - const textBuffer = createTextBuffer(text, TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL); + const textBuffer = createTextBuffer(text, TextModel.DEFAULT_CREATION_OPTIONS.defaultEOL).textBuffer; let actual: ITextBufferData = { EOL: textBuffer.getEOL(), lines: textBuffer.getLinesContent(), containsRTL: textBuffer.mightContainRTL(), isBasicASCII: !textBuffer.mightContainNonBasicASCII() }; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('one line text', () => { @@ -164,30 +164,30 @@ suite('Editor Model - TextModel', () => { test('getValueLengthInRange', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\r\nM'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\r\nMy Second Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\r\nM'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\r\nMy Second Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\r\nMy Second Line\r\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); m = createTextModel('My First Line\nMy Second Line\nMy Third Line'); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\nM'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\nMy Second Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); - assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1, 14)), 'My First Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 2, 1)), 'My First Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1)), 'y First Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 2)), 'y First Line\nM'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 2, 1000)), 'y First Line\nMy Second Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1)), 'y First Line\nMy Second Line\n'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\nMy Second Line\nMy Third Line'.length); + assert.strictEqual(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\nMy Second Line\nMy Third Line'.length); }); test('guess indentation 1', () => { @@ -664,69 +664,69 @@ suite('Editor Model - TextModel', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 30)), new Position(1, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 30)), new Position(1, 9)); - assert.deepEqual(m.validatePosition(new Position(2, 0)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(2, 1)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(2, 2)), new Position(2, 2)); - assert.deepEqual(m.validatePosition(new Position(2, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 2)), new Position(2, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 0)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 1)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(3, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 0)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 1)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(3, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(30, 30)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(30, 30)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); - assert.deepEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(2, 9)); + assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(2, 9)); }); test('validatePosition around high-low surrogate pairs 1', () => { let m = createTextModel('a📚b'); - assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(0, 7)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(0, 7)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 5)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(1, 30)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 5)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 30)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 0)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 1)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 2)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(2, 30)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 2)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(2, 30)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(-123.123, -0.5)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MIN_VALUE, Number.MIN_VALUE)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); - assert.deepEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(Number.MAX_VALUE, Number.MAX_VALUE)), new Position(1, 5)); + assert.deepStrictEqual(m.validatePosition(new Position(123.23, 47.5)), new Position(1, 5)); }); test('validatePosition around high-low surrogate pairs 2', () => { let m = createTextModel('a📚📚b'); - assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); - assert.deepEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 5)), new Position(1, 4)); - assert.deepEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); - assert.deepEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 3)), new Position(1, 2)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 4)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 5)), new Position(1, 4)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 6)), new Position(1, 6)); + assert.deepStrictEqual(m.validatePosition(new Position(1, 7)), new Position(1, 7)); }); @@ -734,133 +734,133 @@ suite('Editor Model - TextModel', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); - assert.deepEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); - assert.deepEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, NaN)), new Position(1, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(2, NaN)), new Position(2, 1)); + assert.deepStrictEqual(m.validatePosition(new Position(NaN, 3)), new Position(1, 3)); }); test('issue #71480: validatePosition handle floats', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); - assert.deepEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); - assert.deepEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); - assert.deepEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); - assert.deepEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); - assert.deepEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); - assert.deepEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); - assert.deepEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); + assert.deepStrictEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); + assert.deepStrictEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); + assert.deepStrictEqual(m.validatePosition(new Position(1.5, 2)), new Position(1, 2), 'c'); + assert.deepStrictEqual(m.validatePosition(new Position(1.8, 3)), new Position(1, 3), 'd'); + assert.deepStrictEqual(m.validatePosition(new Position(1, 0.3)), new Position(1, 1), 'e'); + assert.deepStrictEqual(m.validatePosition(new Position(2, 0.8)), new Position(2, 1), 'f'); + assert.deepStrictEqual(m.validatePosition(new Position(1, 1.2)), new Position(1, 1), 'g'); + assert.deepStrictEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); }); test('issue #71480: validateRange handle floats', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); }); test('validateRange around high-low surrogate pairs 1', () => { let m = createTextModel('a📚b'); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 5)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 5, 1, 5)); }); test('validateRange around high-low surrogate pairs 2', () => { let m = createTextModel('a📚📚b'); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 6)), new Range(1, 1, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 1, 1, 7)), new Range(1, 1, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 1)), new Range(1, 1, 1, 1)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 2)), new Range(1, 1, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 3)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 4)), new Range(1, 1, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 5)), new Range(1, 1, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 6)), new Range(1, 1, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 1, 1, 7)), new Range(1, 1, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 6)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 2, 1, 7)), new Range(1, 2, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 2)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 3)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 5)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 6)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 2, 1, 7)), new Range(1, 2, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 6)), new Range(1, 2, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 3, 1, 7)), new Range(1, 2, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 3)), new Range(1, 2, 1, 2)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 4)), new Range(1, 2, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 5)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 6)), new Range(1, 2, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 3, 1, 7)), new Range(1, 2, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 6)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 4, 1, 7)), new Range(1, 4, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 4)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 5)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 6)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 4, 1, 7)), new Range(1, 4, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 4, 1, 4)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 6)), new Range(1, 4, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 5, 1, 7)), new Range(1, 4, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 5)), new Range(1, 4, 1, 4)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 6)), new Range(1, 4, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 5, 1, 7)), new Range(1, 4, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 6, 1, 6)), new Range(1, 6, 1, 6)); - assert.deepEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 6)), new Range(1, 6, 1, 6)); + assert.deepStrictEqual(m.validateRange(new Range(1, 6, 1, 7)), new Range(1, 6, 1, 7)); - assert.deepEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); + assert.deepStrictEqual(m.validateRange(new Range(1, 7, 1, 7)), new Range(1, 7, 1, 7)); }); test('modifyPosition', () => { let m = createTextModel('line one\nline two'); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 17), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 1), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(1, 1), 3), new Position(1, 4)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 10), new Position(2, 3)); - assert.deepEqual(m.modifyPosition(new Position(1, 5), 13), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 16), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 17), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 1), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 1), 3), new Position(1, 4)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 10), new Position(2, 3)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 5), 13), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 16), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -17), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -1), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 4), -3), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 3), -10), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -13), new Position(1, 5)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -16), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -17), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -1), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 4), -3), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 3), -10), new Position(1, 2)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -13), new Position(1, 5)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -16), new Position(1, 2)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 17), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), 100), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 17), new Position(2, 9)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), 100), new Position(2, 9)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -2), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); - assert.deepEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -2), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(1, 2), -100), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 2), -100), new Position(1, 1)); + assert.deepStrictEqual(m.modifyPosition(new Position(2, 9), -18), new Position(1, 1)); }); test('normalizeIndentation 1', () => { @@ -870,27 +870,27 @@ suite('Editor Model - TextModel', () => { } ); - assert.equal(model.normalizeIndentation('\t'), '\t'); - assert.equal(model.normalizeIndentation(' '), '\t'); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(' '), ' '); - assert.equal(model.normalizeIndentation(''), ''); - assert.equal(model.normalizeIndentation(' \t '), '\t\t'); - assert.equal(model.normalizeIndentation(' \t '), '\t '); - assert.equal(model.normalizeIndentation(' \t '), '\t '); - assert.equal(model.normalizeIndentation(' \t'), '\t '); + assert.strictEqual(model.normalizeIndentation('\t'), '\t'); + assert.strictEqual(model.normalizeIndentation(' '), '\t'); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(' '), ' '); + assert.strictEqual(model.normalizeIndentation(''), ''); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t\t'); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t'), '\t '); - assert.equal(model.normalizeIndentation('\ta'), '\ta'); - assert.equal(model.normalizeIndentation(' a'), '\ta'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation('a'), 'a'); - assert.equal(model.normalizeIndentation(' \t a'), '\t\ta'); - assert.equal(model.normalizeIndentation(' \t a'), '\t a'); - assert.equal(model.normalizeIndentation(' \t a'), '\t a'); - assert.equal(model.normalizeIndentation(' \ta'), '\t a'); + assert.strictEqual(model.normalizeIndentation('\ta'), '\ta'); + assert.strictEqual(model.normalizeIndentation(' a'), '\ta'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation('a'), 'a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t\ta'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), '\t a'); model.dispose(); }); @@ -898,16 +898,16 @@ suite('Editor Model - TextModel', () => { test('normalizeIndentation 2', () => { let model = createTextModel(''); - assert.equal(model.normalizeIndentation('\ta'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation(' a'), ' a'); - assert.equal(model.normalizeIndentation('a'), 'a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \t a'), ' a'); - assert.equal(model.normalizeIndentation(' \ta'), ' a'); + assert.strictEqual(model.normalizeIndentation('\ta'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' a'), ' a'); + assert.strictEqual(model.normalizeIndentation('a'), 'a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), ' a'); model.dispose(); }); @@ -928,18 +928,18 @@ suite('Editor Model - TextModel', () => { '' ].join('\n')); - assert.equal(model.getLineFirstNonWhitespaceColumn(1), 1, '1'); - assert.equal(model.getLineFirstNonWhitespaceColumn(2), 2, '2'); - assert.equal(model.getLineFirstNonWhitespaceColumn(3), 2, '3'); - assert.equal(model.getLineFirstNonWhitespaceColumn(4), 3, '4'); - assert.equal(model.getLineFirstNonWhitespaceColumn(5), 3, '5'); - assert.equal(model.getLineFirstNonWhitespaceColumn(6), 0, '6'); - assert.equal(model.getLineFirstNonWhitespaceColumn(7), 0, '7'); - assert.equal(model.getLineFirstNonWhitespaceColumn(8), 0, '8'); - assert.equal(model.getLineFirstNonWhitespaceColumn(9), 0, '9'); - assert.equal(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); - assert.equal(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); - assert.equal(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(1), 1, '1'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(2), 2, '2'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(3), 2, '3'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(4), 3, '4'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(5), 3, '5'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(6), 0, '6'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(7), 0, '7'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(8), 0, '8'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(9), 0, '9'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(10), 4, '10'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(11), 0, '11'); + assert.strictEqual(model.getLineFirstNonWhitespaceColumn(12), 0, '12'); }); test('getLineLastNonWhitespaceColumn', () => { @@ -958,24 +958,24 @@ suite('Editor Model - TextModel', () => { '' ].join('\n')); - assert.equal(model.getLineLastNonWhitespaceColumn(1), 4, '1'); - assert.equal(model.getLineLastNonWhitespaceColumn(2), 4, '2'); - assert.equal(model.getLineLastNonWhitespaceColumn(3), 4, '3'); - assert.equal(model.getLineLastNonWhitespaceColumn(4), 4, '4'); - assert.equal(model.getLineLastNonWhitespaceColumn(5), 4, '5'); - assert.equal(model.getLineLastNonWhitespaceColumn(6), 0, '6'); - assert.equal(model.getLineLastNonWhitespaceColumn(7), 0, '7'); - assert.equal(model.getLineLastNonWhitespaceColumn(8), 0, '8'); - assert.equal(model.getLineLastNonWhitespaceColumn(9), 0, '9'); - assert.equal(model.getLineLastNonWhitespaceColumn(10), 4, '10'); - assert.equal(model.getLineLastNonWhitespaceColumn(11), 0, '11'); - assert.equal(model.getLineLastNonWhitespaceColumn(12), 0, '12'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(1), 4, '1'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(2), 4, '2'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(3), 4, '3'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(4), 4, '4'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(5), 4, '5'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(6), 0, '6'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(7), 0, '7'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(8), 0, '8'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(9), 0, '9'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(10), 4, '10'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(11), 0, '11'); + assert.strictEqual(model.getLineLastNonWhitespaceColumn(12), 0, '12'); }); test('#50471. getValueInRange with invalid range', () => { let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); - assert.equal(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); - assert.equal(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); + assert.strictEqual(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); + assert.strictEqual(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); }); }); @@ -983,26 +983,26 @@ suite('TextModel.mightContainRTL', () => { test('nope', () => { let model = createTextModel('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); }); test('yes', () => { let model = createTextModel('Hello,\nזוהי עובדה מבוססת שדעתו'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); }); test('setValue resets 1', () => { let model = createTextModel('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); model.setValue('Hello,\nזוהי עובדה מבוססת שדעתו'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); }); test('setValue resets 2', () => { let model = createTextModel('Hello,\nهناك حقيقة مثبتة منذ زمن طويل'); - assert.equal(model.mightContainRTL(), true); + assert.strictEqual(model.mightContainRTL(), true); model.setValue('hello world!'); - assert.equal(model.mightContainRTL(), false); + assert.strictEqual(model.mightContainRTL(), false); }); }); @@ -1012,24 +1012,24 @@ suite('TextModel.createSnapshot', () => { test('empty file', () => { let model = createTextModel(''); let snapshot = model.createSnapshot(); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); test('file with BOM', () => { let model = createTextModel(UTF8_BOM_CHARACTER + 'Hello'); - assert.equal(model.getLineContent(1), 'Hello'); + assert.strictEqual(model.getLineContent(1), 'Hello'); let snapshot = model.createSnapshot(true); - assert.equal(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); test('regular file', () => { let model = createTextModel('My First Line\n\t\tMy Second Line\n Third Line\n\n1'); let snapshot = model.createSnapshot(); - assert.equal(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + assert.strictEqual(snapshot.read(), null); model.dispose(); }); @@ -1054,10 +1054,10 @@ suite('TextModel.createSnapshot', () => { // all good } else { actual += tmp2; - assert.equal(snapshot.read(), null); + assert.strictEqual(snapshot.read(), null); } - assert.equal(actual, text); + assert.strictEqual(actual, text); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 711c010c67..a8140bd683 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -19,31 +19,31 @@ suite('TextModelSearch', () => { const usualWordSeparators = getMapForWordSeparators(USUAL_WORD_SEPARATORS); function assertFindMatch(actual: FindMatch | null, expectedRange: Range, expectedMatches: string[] | null = null): void { - assert.deepEqual(actual, new FindMatch(expectedRange, expectedMatches)); + assert.deepStrictEqual(actual, new FindMatch(expectedRange, expectedMatches)); } function _assertFindMatches(model: TextModel, searchParams: SearchParams, expectedMatches: FindMatch[]): void { let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), false, 1000); - assert.deepEqual(actual, expectedMatches, 'findMatches OK'); + assert.deepStrictEqual(actual, expectedMatches, 'findMatches OK'); // test `findNextMatch` let startPos = new Position(1, 1); let match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatches[0], `findNextMatch ${startPos}`); for (const expectedMatch of expectedMatches) { startPos = expectedMatch.range.getStartPosition(); match = TextModelSearch.findNextMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatch, `findNextMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatch, `findNextMatch ${startPos}`); } // test `findPrevMatch` startPos = new Position(model.getLineCount(), model.getLineMaxColumn(model.getLineCount())); match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatches[expectedMatches.length - 1], `findPrevMatch ${startPos}`); for (const expectedMatch of expectedMatches) { startPos = expectedMatch.range.getEndPosition(); match = TextModelSearch.findPreviousMatch(model, searchParams, startPos, false); - assert.deepEqual(match, expectedMatch, `findPrevMatch ${startPos}`); + assert.deepStrictEqual(match, expectedMatch, `findPrevMatch ${startPos}`); } } @@ -486,7 +486,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('(l(in)e)', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 5, 1, 9), ['line', 'line', 'in']), new FindMatch(new Range(1, 10, 1, 14), ['line', 'line', 'in']), new FindMatch(new Range(2, 5, 2, 9), ['line', 'line', 'in']), @@ -501,7 +501,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('(l(in)e)\\n', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 10, 2, 1), ['line\n', 'line', 'in']), new FindMatch(new Range(2, 5, 3, 1), ['line\n', 'line', 'in']), ]); @@ -556,7 +556,7 @@ suite('TextModelSearch', () => { test('\\n matches \\r\\n', () => { let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); - assert.equal(model.getEOL(), '\r\n'); + assert.strictEqual(model.getEOL(), '\r\n'); let searchParams = new SearchParams('h\\n', true, false, null); let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); @@ -579,12 +579,12 @@ suite('TextModelSearch', () => { test('\\r can never be found', () => { let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); - assert.equal(model.getEOL(), '\r\n'); + assert.strictEqual(model.getEOL(), '\r\n'); let searchParams = new SearchParams('\\r\\n', true, false, null); let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), true); - assert.equal(actual, null); - assert.deepEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); + assert.strictEqual(actual, null); + assert.deepStrictEqual(TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 1000), []); model.dispose(); }); @@ -596,8 +596,8 @@ suite('TextModelSearch', () => { if (expected === null) { assert.ok(actual === null); } else { - assert.deepEqual(actual!.regex, expected.regex); - assert.deepEqual(actual!.simpleSearch, expected.simpleSearch); + assert.deepStrictEqual(actual!.regex, expected.regex); + assert.deepStrictEqual(actual!.simpleSearch, expected.simpleSearch); if (wordSeparators) { assert.ok(actual!.wordSeparators !== null); } else { @@ -769,7 +769,7 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('\\d*', true, false, null); let actual = TextModelSearch.findMatches(model, searchParams, model.getFullModelRange(), true, 100); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ new FindMatch(new Range(1, 1, 1, 3), ['10']), new FindMatch(new Range(1, 3, 1, 3), ['']), new FindMatch(new Range(1, 4, 1, 7), ['243']), diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index 0c3c13fefe..efdca42170 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -100,7 +100,7 @@ suite('TextModelWithTokens', () => { column: column }); - assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); + assert.deepStrictEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findPrevBracket of ' + lineNumber + ', ' + column); } } } @@ -126,7 +126,7 @@ suite('TextModelWithTokens', () => { column: column }); - assert.deepEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); + assert.deepStrictEqual(toRelaxedFoundBracket(actual), toRelaxedFoundBracket(currentExpectedBracket), 'findNextBracket of ' + lineNumber + ', ' + column); } } } @@ -148,12 +148,12 @@ suite('TextModelWithTokens', () => { function assertIsNotBracket(model: TextModel, lineNumber: number, column: number) { const match = model.matchBracket(new Position(lineNumber, column)); - assert.equal(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); + assert.strictEqual(match, null, 'is not matching brackets at ' + lineNumber + ', ' + column); } function assertIsBracket(model: TextModel, testPosition: Position, expected: [Range, Range]): void { const actual = model.matchBracket(testPosition); - assert.deepEqual(actual, expected, 'matches brackets at ' + testPosition); + assert.deepStrictEqual(actual, expected, 'matches brackets at ' + testPosition); } suite('TextModelWithTokens - bracket matching', () => { @@ -351,7 +351,7 @@ suite('TextModelWithTokens', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { switch (line) { case 'function hello() {': { const tokens = new Uint32Array([ @@ -399,8 +399,8 @@ suite('TextModelWithTokens', () => { model.forceTokenization(2); model.forceTokenization(3); - assert.deepEqual(model.matchBracket(new Position(2, 23)), null); - assert.deepEqual(model.matchBracket(new Position(2, 20)), null); + assert.deepStrictEqual(model.matchBracket(new Position(2, 23)), null); + assert.deepStrictEqual(model.matchBracket(new Position(2, 20)), null); model.dispose(); registration1.dispose(); @@ -434,7 +434,7 @@ suite('TextModelWithTokens regression tests', () => { foreground: token.getForeground() }; }; - assert.deepEqual(actual, expected.map(decode)); + assert.deepStrictEqual(actual, expected.map(decode)); } let _tokenId = 10; @@ -446,7 +446,7 @@ suite('TextModelWithTokens regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { let myId = ++_tokenId; let tokens = new Uint32Array(2); tokens[0] = 0; @@ -512,7 +512,7 @@ suite('TextModelWithTokens regression tests', () => { ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(4, 1)); - assert.deepEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); + assert.deepStrictEqual(actual, [new Range(4, 1, 4, 7), new Range(9, 1, 9, 11)]); model.dispose(); registration.dispose(); @@ -537,7 +537,7 @@ suite('TextModelWithTokens regression tests', () => { ].join('\n'), undefined, languageIdentifier); let actual = model.matchBracket(new Position(3, 9)); - assert.deepEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); + assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); model.dispose(); registration.dispose(); @@ -550,7 +550,7 @@ suite('TextModelWithTokens regression tests', () => { const tokenizationSupport: ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line, state) => { + tokenize2: (line, hasEOL, state) => { let tokens = new Uint32Array(2); tokens[0] = 0; tokens[1] = ( @@ -565,7 +565,7 @@ suite('TextModelWithTokens regression tests', () => { let model = createTextModel('A model with one line', undefined, outerMode); model.forceTokenization(1); - assert.equal(model.getLanguageIdAtPosition(1, 1), innerMode.id); + assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode.id); model.dispose(); registration.dispose(); @@ -586,7 +586,7 @@ suite('TextModel.getLineIndentGuide', () => { actual[line - 1] = [actualIndents[line - 1], activeIndentGuide.startLineNumber, activeIndentGuide.endLineNumber, activeIndentGuide.indent, model.getLineContent(line)]; } - assert.deepEqual(actual, lines); + assert.deepStrictEqual(actual, lines); model.dispose(); } @@ -764,7 +764,7 @@ suite('TextModel.getLineIndentGuide', () => { ].join('\n')); const actual = model.getActiveIndentGuide(2, 4, 9); - assert.deepEqual(actual, { startLineNumber: 2, endLineNumber: 9, indent: 1 }); + assert.deepStrictEqual(actual, { startLineNumber: 2, endLineNumber: 9, indent: 1 }); model.dispose(); }); diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index f7a21b707e..97ee360005 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -104,7 +104,7 @@ suite('TokensStore', () => { model.applyEdits(edits); const actualState = extractState(model); - assert.deepEqual(actualState, rawFinalState); + assert.deepStrictEqual(actualState, rawFinalState); model.dispose(); } @@ -191,7 +191,7 @@ suite('TokensStore', () => { decodedTokens.push(lineTokens.getEndOffset(i), lineTokens.getMetadata(i)); } - assert.deepEqual(decodedTokens, [ + assert.deepStrictEqual(decodedTokens, [ 20, 16793600, 24, 17022976, 25, 16793600, @@ -252,7 +252,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 2', () => { @@ -293,7 +293,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('partial tokens 3', () => { @@ -320,7 +320,7 @@ suite('TokensStore', () => { ]); const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 3); + assert.strictEqual(lineTokens.getCount(), 3); }); test('issue #94133: Semantic colors stick around when using (only) range provider', () => { @@ -337,7 +337,7 @@ suite('TokensStore', () => { store.setPartial(new Range(1, 1, 1, 20), []); const lineTokens = store.addSemanticTokens(1, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`)); - assert.equal(lineTokens.getCount(), 1); + assert.strictEqual(lineTokens.getCount(), 1); }); test('bug', () => { @@ -385,7 +385,7 @@ suite('TokensStore', () => { ); const lineTokens = store.addSemanticTokens(36451, new LineTokens(new Uint32Array([60, 1]), ` if (flags & ModifierFlags.Ambient) {`)); - assert.equal(lineTokens.getCount(), 7); + assert.strictEqual(lineTokens.getCount(), 7); }); @@ -424,7 +424,7 @@ suite('TokensStore', () => { ]), `const hello = 123;`)); const actual = toArr(lineTokens); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ 5, createTMMetadata(5, FontStyle.Bold, 53), 6, createTMMetadata(1, FontStyle.None, 53), 11, createTMMetadata(1, FontStyle.None, 53), diff --git a/src/vs/editor/test/common/modes/languageConfiguration.test.ts b/src/vs/editor/test/common/modes/languageConfiguration.test.ts index 33b674670b..8761506d08 100644 --- a/src/vs/editor/test/common/modes/languageConfiguration.test.ts +++ b/src/vs/editor/test/common/modes/languageConfiguration.test.ts @@ -4,88 +4,98 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { StandardTokenType } from 'vs/editor/common/modes'; +import { LanguageIdentifier, StandardTokenType } from 'vs/editor/common/modes'; import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; suite('StandardAutoClosingPairConditional', () => { test('Missing notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}' }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('Empty notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: [] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('Invalid notIn', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['bla'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in strings', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in comments', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['comment'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in strings nor comments', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'comment'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), true); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), true); }); test('notIn in strings nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), true); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), true); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in comments nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['comment', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), true); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), true); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); }); test('notIn in strings, comments nor regex', () => { let v = new StandardAutoClosingPairConditional({ open: '{', close: '}', notIn: ['string', 'comment', 'regex'] }); - assert.equal(v.isOK(StandardTokenType.Other), true); - assert.equal(v.isOK(StandardTokenType.Comment), false); - assert.equal(v.isOK(StandardTokenType.String), false); - assert.equal(v.isOK(StandardTokenType.RegEx), false); + assert.strictEqual(v.isOK(StandardTokenType.Other), true); + assert.strictEqual(v.isOK(StandardTokenType.Comment), false); + assert.strictEqual(v.isOK(StandardTokenType.String), false); + assert.strictEqual(v.isOK(StandardTokenType.RegEx), false); + }); + + test('language configurations priorities', () => { + const id = new LanguageIdentifier('testLang1', 15); + const d1 = LanguageConfigurationRegistry.register(id, { comments: { lineComment: '1' } }, 100); + const d2 = LanguageConfigurationRegistry.register(id, { comments: { lineComment: '2' } }, 10); + assert.strictEqual(LanguageConfigurationRegistry.getComments(id.id)?.lineCommentToken, '1'); + d1.dispose(); + d2.dispose(); }); }); diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index d09a5866a2..cbb590ea69 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -15,18 +15,18 @@ suite('LanguageSelector', function () { }; test('score, invalid selector', function () { - assert.equal(score({}, model.uri, model.language, true), 0); - assert.equal(score(undefined!, model.uri, model.language, true), 0); - assert.equal(score(null!, model.uri, model.language, true), 0); - assert.equal(score('', model.uri, model.language, true), 0); + assert.strictEqual(score({}, model.uri, model.language, true), 0); + assert.strictEqual(score(undefined!, model.uri, model.language, true), 0); + assert.strictEqual(score(null!, model.uri, model.language, true), 0); + assert.strictEqual(score('', model.uri, model.language, true), 0); }); test('score, any language', function () { - assert.equal(score({ language: '*' }, model.uri, model.language, true), 5); - assert.equal(score('*', model.uri, model.language, true), 5); + assert.strictEqual(score({ language: '*' }, model.uri, model.language, true), 5); + assert.strictEqual(score('*', model.uri, model.language, true), 5); - assert.equal(score('*', URI.parse('foo:bar'), model.language, true), 5); - assert.equal(score('farboo', URI.parse('foo:bar'), model.language, true), 10); + assert.strictEqual(score('*', URI.parse('foo:bar'), model.language, true), 5); + assert.strictEqual(score('farboo', URI.parse('foo:bar'), model.language, true), 10); }); test('score, default schemes', function () { @@ -34,50 +34,50 @@ suite('LanguageSelector', function () { const uri = URI.parse('git:foo/file.txt'); const language = 'farboo'; - assert.equal(score('*', uri, language, true), 5); - assert.equal(score('farboo', uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: '' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'git' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo', scheme: '*' }, uri, language, true), 10); - assert.equal(score({ language: 'farboo' }, uri, language, true), 10); - assert.equal(score({ language: '*' }, uri, language, true), 5); + assert.strictEqual(score('*', uri, language, true), 5); + assert.strictEqual(score('farboo', uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'git' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '*' }, uri, language, true), 10); + assert.strictEqual(score({ language: 'farboo' }, uri, language, true), 10); + assert.strictEqual(score({ language: '*' }, uri, language, true), 5); - assert.equal(score({ scheme: '*' }, uri, language, true), 5); - assert.equal(score({ scheme: 'git' }, uri, language, true), 10); + assert.strictEqual(score({ scheme: '*' }, uri, language, true), 5); + assert.strictEqual(score({ scheme: 'git' }, uri, language, true), 10); }); test('score, filter', function () { - assert.equal(score('farboo', model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo' }, model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true), 10); - assert.equal(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true), 0); + assert.strictEqual(score('farboo', model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true), 0); - assert.equal(score({ pattern: '**/*.fb' }, model.uri, model.language, true), 10); - assert.equal(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true), 10); - assert.equal(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true), 0); - assert.equal(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true), 0); + assert.strictEqual(score({ pattern: '**/*.fb' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true), 10); + assert.strictEqual(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true), 0); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true), 0); let doc = { uri: URI.parse('git:/my/file.js'), langId: 'javascript' }; - assert.equal(score('javascript', doc.uri, doc.langId, true), 10); // 0; - assert.equal(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true), 10); // 10; - assert.equal(score('*', doc.uri, doc.langId, true), 5); // 5 - assert.equal(score('fooLang', doc.uri, doc.langId, true), 0); // 0 - assert.equal(score(['fooLang', '*'], doc.uri, doc.langId, true), 5); // 5 + assert.strictEqual(score('javascript', doc.uri, doc.langId, true), 10); // 0; + assert.strictEqual(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true), 10); // 10; + assert.strictEqual(score('*', doc.uri, doc.langId, true), 5); // 5 + assert.strictEqual(score('fooLang', doc.uri, doc.langId, true), 0); // 0 + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, true), 5); // 5 }); test('score, max(filters)', function () { let match = { language: 'farboo', scheme: 'file' }; let fail = { language: 'farboo', scheme: 'http' }; - assert.equal(score(match, model.uri, model.language, true), 10); - assert.equal(score(fail, model.uri, model.language, true), 0); - assert.equal(score([match, fail], model.uri, model.language, true), 10); - assert.equal(score([fail, fail], model.uri, model.language, true), 0); - assert.equal(score(['farboo', '*'], model.uri, model.language, true), 10); - assert.equal(score(['*', 'farboo'], model.uri, model.language, true), 10); + assert.strictEqual(score(match, model.uri, model.language, true), 10); + assert.strictEqual(score(fail, model.uri, model.language, true), 0); + assert.strictEqual(score([match, fail], model.uri, model.language, true), 10); + assert.strictEqual(score([fail, fail], model.uri, model.language, true), 0); + assert.strictEqual(score(['farboo', '*'], model.uri, model.language, true), 10); + assert.strictEqual(score(['*', 'farboo'], model.uri, model.language, true), 10); }); test('score hasAccessToAllModels', function () { @@ -85,14 +85,14 @@ suite('LanguageSelector', function () { uri: URI.parse('file:/my/file.js'), langId: 'javascript' }; - assert.equal(score('javascript', doc.uri, doc.langId, false), 0); - assert.equal(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false), 0); - assert.equal(score('*', doc.uri, doc.langId, false), 0); - assert.equal(score('fooLang', doc.uri, doc.langId, false), 0); - assert.equal(score(['fooLang', '*'], doc.uri, doc.langId, false), 0); + assert.strictEqual(score('javascript', doc.uri, doc.langId, false), 0); + assert.strictEqual(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false), 0); + assert.strictEqual(score('*', doc.uri, doc.langId, false), 0); + assert.strictEqual(score('fooLang', doc.uri, doc.langId, false), 0); + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, false), 0); - assert.equal(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false), 10); - assert.equal(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false), 5); + assert.strictEqual(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false), 10); + assert.strictEqual(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false), 5); }); test('Document selector match - unexpected result value #60232', function () { @@ -102,7 +102,7 @@ suite('LanguageSelector', function () { pattern: '**/*.interface.json' }; let value = score(selector, URI.parse('file:///C:/Users/zlhe/Desktop/test.interface.json'), 'json', true); - assert.equal(value, 10); + assert.strictEqual(value, 10); }); test('Document selector match - platform paths #99938', function () { @@ -113,6 +113,6 @@ suite('LanguageSelector', function () { } }; let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true); - assert.equal(value, 10); + assert.strictEqual(value, 10); }); }); diff --git a/src/vs/editor/test/common/modes/linkComputer.test.ts b/src/vs/editor/test/common/modes/linkComputer.test.ts index 00db81895a..13974ff90a 100644 --- a/src/vs/editor/test/common/modes/linkComputer.test.ts +++ b/src/vs/editor/test/common/modes/linkComputer.test.ts @@ -49,7 +49,7 @@ function assertLink(text: string, extractedLink: string): void { } let r = myComputeLinks([text]); - assert.deepEqual(r, [{ + assert.deepStrictEqual(r, [{ range: { startLineNumber: 1, startColumn: startColumn, @@ -64,7 +64,7 @@ suite('Editor Modes - Link Computer', () => { test('Null model', () => { let r = computeLinks(null); - assert.deepEqual(r, []); + assert.deepStrictEqual(r, []); }); test('Parsing', () => { diff --git a/src/vs/editor/test/common/modes/supports/characterPair.test.ts b/src/vs/editor/test/common/modes/supports/characterPair.test.ts index fcafb33296..a89231199d 100644 --- a/src/vs/editor/test/common/modes/supports/characterPair.test.ts +++ b/src/vs/editor/test/common/modes/supports/characterPair.test.ts @@ -13,44 +13,44 @@ suite('CharacterPairSupport', () => { test('only autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [{ open: 'a', close: 'b' }] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); }); test('only empty autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('only brackets', () => { let characaterPairSupport = new CharacterPairSupport({ brackets: [['a', 'b']] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b', _standardTokenMask: 0 }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [new StandardAutoClosingPairConditional({ open: 'a', close: 'b' })]); }); test('only empty brackets', () => { let characaterPairSupport = new CharacterPairSupport({ brackets: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('only surroundingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ surroundingPairs: [{ open: 'a', close: 'b' }] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b' }]); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), [{ open: 'a', close: 'b' }]); }); test('only empty surroundingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ surroundingPairs: [] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); test('brackets is ignored when having autoClosingPairs', () => { let characaterPairSupport = new CharacterPairSupport({ autoClosingPairs: [], brackets: [['a', 'b']] }); - assert.deepEqual(characaterPairSupport.getAutoClosingPairs(), []); - assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getAutoClosingPairs(), []); + assert.deepStrictEqual(characaterPairSupport.getSurroundingPairs(), []); }); function findAutoClosingPair(characterPairSupport: CharacterPairSupport, character: string): StandardAutoClosingPairConditional | undefined { @@ -67,64 +67,64 @@ suite('CharacterPairSupport', () => { test('shouldAutoClosePair in empty line', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [], '{', 1), true); }); test('shouldAutoClosePair in not interesting line 1', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.Other }], 'a', 3), false); }); test('shouldAutoClosePair in not interesting line 2', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}' }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'do', type: StandardTokenType.String }], 'a', 3), false); }); test('shouldAutoClosePair in interesting line 1', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: '"a"', type: StandardTokenType.String }], 'a', 4), false); }); test('shouldAutoClosePair in interesting line 2', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 1), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 2), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 3), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 6), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 6), false); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 7), true); - assert.equal(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 7), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 2), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 3), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 6), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 6), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], '{', 7), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: 'x=', type: StandardTokenType.Other }, { text: '"a"', type: StandardTokenType.String }, { text: ';', type: StandardTokenType.Other }], 'a', 7), false); }); test('shouldAutoClosePair in interesting line 3', () => { let sup = new CharacterPairSupport({ autoClosingPairs: [{ open: '{', close: '}', notIn: ['string', 'comment'] }] }); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 1), true); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 1), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 2), true); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 2), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 3), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 4), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 5), false); - assert.equal(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 1), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 1), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 2), true); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 2), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 3), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 4), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], '{', 5), false); + assert.strictEqual(testShouldAutoClose(sup, [{ text: ' ', type: StandardTokenType.Other }, { text: '//a', type: StandardTokenType.Comment }], 'a', 5), false); }); }); diff --git a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts index 30d0608626..09e494dc26 100644 --- a/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts +++ b/src/vs/editor/test/common/modes/supports/electricCharacter.test.ts @@ -18,12 +18,12 @@ suite('Editor Modes - Auto Indentation', () => { function testDoesNothing(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, null); + assert.deepStrictEqual(actual, null); } function testMatchBracket(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, matchOpenBracket: string): void { let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset); - assert.deepEqual(actual, { matchOpenBracket: matchOpenBracket }); + assert.deepStrictEqual(actual, { matchOpenBracket: matchOpenBracket }); } test('getElectricCharacters uses all sources and dedups', () => { @@ -34,7 +34,7 @@ suite('Editor Modes - Auto Indentation', () => { ]) ); - assert.deepEqual(sup.getElectricCharacters(), ['}', ')']); + assert.deepStrictEqual(sup.getElectricCharacters(), ['}', ')']); }); test('matchOpenBracket', () => { diff --git a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts index 7f9bf4de7d..ebacdbc777 100644 --- a/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/javascriptOnEnterRules.ts @@ -18,7 +18,7 @@ export const javascriptOnEnterRules = [ }, { // e.g. * ...| beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, - oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, + previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, action: { indentAction: IndentAction.None, appendText: '* ' } }, { // e.g. */| diff --git a/src/vs/editor/test/common/modes/supports/onEnter.test.ts b/src/vs/editor/test/common/modes/supports/onEnter.test.ts index df3c92b235..05d67e27b1 100644 --- a/src/vs/editor/test/common/modes/supports/onEnter.test.ts +++ b/src/vs/editor/test/common/modes/supports/onEnter.test.ts @@ -21,9 +21,9 @@ suite('OnEnter', () => { let testIndentAction = (beforeText: string, afterText: string, expected: IndentAction) => { let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, '', beforeText, afterText); if (expected === IndentAction.None) { - assert.equal(actual, null); + assert.strictEqual(actual, null); } else { - assert.equal(actual!.indentAction, expected); + assert.strictEqual(actual!.indentAction, expected); } }; @@ -51,18 +51,18 @@ suite('OnEnter', () => { let support = new OnEnterSupport({ onEnterRules: javascriptOnEnterRules }); - let testIndentAction = (oneLineAboveText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { - let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, oneLineAboveText, beforeText, afterText); + let testIndentAction = (previousLineText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => { + let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, previousLineText, beforeText, afterText); if (expectedIndentAction === null) { - assert.equal(actual, null, 'isNull:' + beforeText); + assert.strictEqual(actual, null, 'isNull:' + beforeText); } else { - assert.equal(actual !== null, true, 'isNotNull:' + beforeText); - assert.equal(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); + assert.strictEqual(actual !== null, true, 'isNotNull:' + beforeText); + assert.strictEqual(actual!.indentAction, expectedIndentAction, 'indentAction:' + beforeText); if (expectedAppendText !== null) { - assert.equal(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); + assert.strictEqual(actual!.appendText, expectedAppendText, 'appendText:' + beforeText); } if (removeText !== 0) { - assert.equal(actual!.removeText, removeText, 'removeText:' + beforeText); + assert.strictEqual(actual!.removeText, removeText, 'removeText:' + beforeText); } } }; diff --git a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts index 9f6c5ad309..859a9e24ee 100644 --- a/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts +++ b/src/vs/editor/test/common/modes/supports/richEditBrackets.test.ts @@ -19,61 +19,61 @@ suite('richEditBrackets', () => { test('findPrevBracketInToken one char 1', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken one char 2', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken one char 3', () => { let result = findPrevBracketInRange(/(\{)|(\})/i, '{hello world!', 0, 13); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findPrevBracketInToken more chars 1', () => { let result = findPrevBracketInRange(/(olleh)/i, 'hello world!', 0, 12); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 6); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 6); }); test('findPrevBracketInToken more chars 2', () => { let result = findPrevBracketInRange(/(olleh)/i, 'hello world!', 0, 5); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 6); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 6); }); test('findPrevBracketInToken more chars 3', () => { let result = findPrevBracketInRange(/(olleh)/i, ' hello world!', 0, 6); - assert.equal(result!.startColumn, 2); - assert.equal(result!.endColumn, 7); + assert.strictEqual(result!.startColumn, 2); + assert.strictEqual(result!.endColumn, 7); }); test('findNextBracketInToken one char', () => { let result = findNextBracketInRange(/(\{)|(\})/i, '{', 0, 1); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 2); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 2); }); test('findNextBracketInToken more chars', () => { let result = findNextBracketInRange(/(world)/i, 'hello world!', 0, 12); - assert.equal(result!.startColumn, 7); - assert.equal(result!.endColumn, 12); + assert.strictEqual(result!.startColumn, 7); + assert.strictEqual(result!.endColumn, 12); }); test('findNextBracketInToken with emoty result', () => { let result = findNextBracketInRange(/(\{)|(\})/i, '', 0, 0); - assert.equal(result, null); + assert.strictEqual(result, null); }); test('issue #3894: [Handlebars] Curly braces edit issues', () => { let result = findPrevBracketInRange(/(\-\-!<)|(>\-\-)|(\{\{)|(\}\})/i, '{{asd}}', 0, 2); - assert.equal(result!.startColumn, 1); - assert.equal(result!.endColumn, 3); + assert.strictEqual(result!.startColumn, 1); + assert.strictEqual(result!.endColumn, 3); }); }); diff --git a/src/vs/editor/test/common/modes/supports/tokenization.test.ts b/src/vs/editor/test/common/modes/supports/tokenization.test.ts index f96f1ada2e..6993c5b0ad 100644 --- a/src/vs/editor/test/common/modes/supports/tokenization.test.ts +++ b/src/vs/editor/test/common/modes/supports/tokenization.test.ts @@ -24,7 +24,7 @@ suite('Token theme matching', () => { let actual = theme._match('punctuation.definition.string.begin.html'); - assert.deepEqual(actual, new ThemeTrieElementRule(FontStyle.None, _D, _B)); + assert.deepStrictEqual(actual, new ThemeTrieElementRule(FontStyle.None, _D, _B)); }); test('can match', () => { @@ -55,7 +55,7 @@ suite('Token theme matching', () => { function assertMatch(scopeName: string, expected: ThemeTrieElementRule): void { let actual = theme._match(scopeName); - assert.deepEqual(actual, expected, 'when matching <<' + scopeName + '>>'); + assert.deepStrictEqual(actual, expected, 'when matching <<' + scopeName + '>>'); } function assertSimpleMatch(scopeName: string, fontStyle: FontStyle, foreground: number, background: number): void { @@ -152,7 +152,7 @@ suite('Token theme parsing', () => { new ParsedTokenThemeRule('constant.numeric.dec', 10, FontStyle.None, '0000ff', null), ]; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); }); @@ -162,7 +162,7 @@ suite('Token theme resolving', () => { let actual = ['bar', 'z', 'zu', 'a', 'ab', ''].sort(strcmp); let expected = ['', 'a', 'ab', 'bar', 'z', 'zu']; - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); test('always has defaults', () => { @@ -170,8 +170,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 1', () => { @@ -181,8 +181,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 2', () => { @@ -192,8 +192,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 3', () => { @@ -203,8 +203,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); }); test('respects incoming defaults 4', () => { @@ -214,8 +214,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('ff0000'); const _B = colorMap.getId('ffffff'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('respects incoming defaults 5', () => { @@ -225,8 +225,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('000000'); const _B = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B))); }); test('can merge incoming defaults', () => { @@ -238,8 +238,8 @@ suite('Token theme resolving', () => { let colorMap = new ColorMap(); const _A = colorMap.getId('00ff00'); const _B = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); - assert.deepEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getThemeTrieElement(), new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _A, _B))); }); test('defaults are inherited', () => { @@ -251,7 +251,7 @@ suite('Token theme resolving', () => { const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _C, _B)) }); @@ -268,7 +268,7 @@ suite('Token theme resolving', () => { const _A = colorMap.getId('F8F8F2'); const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _C, _B)) }); @@ -286,7 +286,7 @@ suite('Token theme resolving', () => { const _B = colorMap.getId('272822'); const _C = colorMap.getId('ff0000'); const _D = colorMap.getId('00ff00'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _C, _B), { 'identifier': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _D, _B)) @@ -314,7 +314,7 @@ suite('Token theme resolving', () => { const _E = colorMap.getId('300000'); const _F = colorMap.getId('ff0000'); const _G = colorMap.getId('00ff00'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); let root = new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.None, _A, _B), { 'var': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _F, _B), { 'identifier': new ExternalThemeTrieElement(new ThemeTrieElementRule(FontStyle.Bold, _G, _B)) @@ -341,6 +341,6 @@ suite('Token theme resolving', () => { colorMap.getId('FFFFFF'); colorMap.getId('0F0F0F'); colorMap.getId('F8F8F2'); - assert.deepEqual(actual.getColorMap(), colorMap.getColorMap()); + assert.deepStrictEqual(actual.getColorMap(), colorMap.getColorMap()); }); }); diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 7de605f9a3..bc4861587f 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -31,7 +31,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]; let expectedStr = `
${toStr(expected)}
`; - assert.equal(actual, expectedStr); + assert.strictEqual(actual, expectedStr); mode.dispose(); }); @@ -61,7 +61,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { let expectedStr2 = toStr(expected2); let expectedStr = `
${expectedStr1}
${expectedStr2}
`; - assert.equal(actual, expectedStr); + assert.strictEqual(actual, expectedStr); mode.dispose(); }); @@ -104,7 +104,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]); const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
', @@ -117,7 +117,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 12, 4, true), [ '
', @@ -130,7 +130,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 11, 4, true), [ '
', @@ -142,7 +142,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 1, 11, 4, true), [ '
', @@ -154,7 +154,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 4, 11, 4, true), [ '
', @@ -165,7 +165,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 5, 11, 4, true), [ '
', @@ -175,7 +175,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 5, 10, 4, true), [ '
', @@ -184,7 +184,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 6, 9, 4, true), [ '
', @@ -237,7 +237,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ]); const colorMap = [null!, '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff']; - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 21, 4, true), [ '
', @@ -251,7 +251,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 17, 4, true), [ '
', @@ -265,7 +265,7 @@ suite('Editor Modes - textToHtmlTokenizer', () => { ].join('') ); - assert.equal( + assert.strictEqual( tokenizeLineToHTML(text, lineTokens, colorMap, 0, 3, 4, true), [ '
', @@ -287,7 +287,7 @@ class Mode extends MockMode { this._register(TokenizationRegistry.register(this.getId(), { getInitialState: (): IState => null!, tokenize: undefined!, - tokenize2: (line: string, state: IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: IState): TokenizationResult2 => { let tokensArr: number[] = []; let prevColor: ColorId = -1; for (let i = 0; i < line.length; i++) { diff --git a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts index 19f096eaa4..f605448904 100644 --- a/src/vs/editor/test/common/services/editorSimpleWorker.test.ts +++ b/src/vs/editor/test/common/services/editorSimpleWorker.test.ts @@ -43,13 +43,13 @@ suite('EditorSimpleWorker', () => { function assertPositionAt(offset: number, line: number, column: number) { let position = model.positionAt(offset); - assert.equal(position.lineNumber, line); - assert.equal(position.column, column); + assert.strictEqual(position.lineNumber, line); + assert.strictEqual(position.column, column); } function assertOffsetAt(lineNumber: number, column: number, offset: number) { let actual = model.offsetAt({ lineNumber, column }); - assert.equal(actual, offset); + assert.strictEqual(actual, offset); } test('ICommonModel#offsetAt', () => { @@ -83,16 +83,16 @@ suite('EditorSimpleWorker', () => { test('ICommonModel#validatePosition, issue #15882', function () { let model = worker.addModel(['{"id": "0001","type": "donut","name": "Cake","image":{"url": "images/0001.jpg","width": 200,"height": 200},"thumbnail":{"url": "images/thumbnails/0001.jpg","width": 32,"height": 32}}']); - assert.equal(model.offsetAt({ lineNumber: 1, column: 2 }), 1); + assert.strictEqual(model.offsetAt({ lineNumber: 1, column: 2 }), 1); }); test('MoreMinimal', () => { return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: 'This is line One', range: new Range(1, 1, 1, 17) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, 'O'); - assert.deepEqual(first.range, { startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 15 }); + assert.strictEqual(first.text, 'O'); + assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 15 }); }); }); @@ -105,7 +105,7 @@ suite('EditorSimpleWorker', () => { ], '\n'); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"a":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => { - assert.equal(edits.length, 0); + assert.strictEqual(edits.length, 0); }); }); @@ -118,10 +118,10 @@ suite('EditorSimpleWorker', () => { ], '\n'); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"b":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, 'b'); - assert.deepEqual(first.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 4 }); + assert.strictEqual(first.text, 'b'); + assert.deepStrictEqual(first.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 4 }); }); }); @@ -134,10 +134,10 @@ suite('EditorSimpleWorker', () => { ]); return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '\n', range: new Range(3, 2, 4, 1000) }]).then(edits => { - assert.equal(edits.length, 1); + assert.strictEqual(edits.length, 1); const [first] = edits; - assert.equal(first.text, '\n'); - assert.deepEqual(first.range, { startLineNumber: 3, startColumn: 2, endLineNumber: 3, endColumn: 2 }); + assert.strictEqual(first.text, '\n'); + assert.deepStrictEqual(first.range, { startLineNumber: 3, startColumn: 2, endLineNumber: 3, endColumn: 2 }); }); }); @@ -151,7 +151,7 @@ suite('EditorSimpleWorker', () => { ]); const value = model.getValueInRange({ startLineNumber: 3, startColumn: 1, endLineNumber: 4, endColumn: 1 }); - assert.equal(value, '}'); + assert.strictEqual(value, '}'); }); @@ -165,11 +165,10 @@ suite('EditorSimpleWorker', () => { return worker.textualSuggest([model.uri.toString()], 'f', '[a-z]+', 'img').then((result) => { if (!result) { assert.ok(false); - return; } - assert.equal(result.words.length, 1); - assert.equal(typeof result.duration, 'number'); - assert.equal(result.words[0], 'foobar'); + assert.strictEqual(result.words.length, 1); + assert.strictEqual(typeof result.duration, 'number'); + assert.strictEqual(result.words[0], 'foobar'); }); }); @@ -187,6 +186,6 @@ suite('EditorSimpleWorker', () => { let words: string[] = [...model.words(/[a-z]+/img)]; - assert.deepEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); + assert.deepStrictEqual(words, ['one', 'line', 'two', 'line', 'past', 'empty', 'single', 'and', 'now', 'we', 'are', 'done']); }); }); diff --git a/src/vs/editor/test/common/services/languagesRegistry.test.ts b/src/vs/editor/test/common/services/languagesRegistry.test.ts index 795c652a5f..cebab44aa6 100644 --- a/src/vs/editor/test/common/services/languagesRegistry.test.ts +++ b/src/vs/editor/test/common/services/languagesRegistry.test.ts @@ -19,7 +19,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['outputModeMimeType'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), []); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), []); }); test('mode with alias does have a name', () => { @@ -32,8 +32,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['ModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'ModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); }); test('mode without alias gets a name', () => { @@ -45,8 +45,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['modeId']); - assert.deepEqual(registry.getLanguageName('modeId'), 'modeId'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['modeId']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'modeId'); }); test('bug #4360: f# not shown in status bar', () => { @@ -66,8 +66,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['ModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'ModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['ModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'ModeName'); }); test('issue #5278: Extension cannot override language name anymore', () => { @@ -87,8 +87,8 @@ suite('LanguagesRegistry', () => { mimetypes: ['bla'], }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); - assert.deepEqual(registry.getLanguageName('modeId'), 'BetterModeName'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['BetterModeName']); + assert.deepStrictEqual(registry.getLanguageName('modeId'), 'BetterModeName'); }); test('mimetypes are generated if necessary', () => { @@ -98,7 +98,7 @@ suite('LanguagesRegistry', () => { id: 'modeId' }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); }); test('first mimetype wins', () => { @@ -109,7 +109,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['text/modeId', 'text/modeId2'] }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/modeId'); }); test('first mimetype wins 2', () => { @@ -124,7 +124,7 @@ suite('LanguagesRegistry', () => { mimetypes: ['text/modeId'] }]); - assert.deepEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); + assert.deepStrictEqual(registry.getMimeForMode('modeId'), 'text/x-modeId'); }); test('aliases', () => { @@ -134,42 +134,42 @@ suite('LanguagesRegistry', () => { id: 'a' }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); registry._registerLanguages([{ id: 'a', aliases: ['A1', 'A2'] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['A1']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'A1'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A1']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'A1'); registry._registerLanguages([{ id: 'a', aliases: ['A3', 'A4'] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['A3']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A1'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A2'), []); - assert.deepEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('A4'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'A3'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['A3']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A1'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A2'), []); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A3'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('A4'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a1'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a2'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a3'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a4'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'A3'); }); test('empty aliases array means no alias', () => { @@ -179,23 +179,23 @@ suite('LanguagesRegistry', () => { id: 'a' }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); registry._registerLanguages([{ id: 'b', aliases: [] }]); - assert.deepEqual(registry.getRegisteredLanguageNames(), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('a'), ['a']); - assert.deepEqual(registry.getModeIdsFromLanguageName('b'), []); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); - assert.deepEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); - assert.deepEqual(registry.getLanguageName('a'), 'a'); - assert.deepEqual(registry.getLanguageName('b'), null); + assert.deepStrictEqual(registry.getRegisteredLanguageNames(), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('a'), ['a']); + assert.deepStrictEqual(registry.getModeIdsFromLanguageName('b'), []); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('a'), 'a'); + assert.deepStrictEqual(registry.getModeIdForLanguageNameLowercase('b'), 'b'); + assert.deepStrictEqual(registry.getLanguageName('a'), 'a'); + assert.deepStrictEqual(registry.getLanguageName('b'), null); }); test('extensions', () => { @@ -207,18 +207,18 @@ suite('LanguagesRegistry', () => { extensions: ['aExt'] }]); - assert.deepEqual(registry.getExtensions('a'), []); - assert.deepEqual(registry.getExtensions('aname'), []); - assert.deepEqual(registry.getExtensions('aName'), ['aExt']); + assert.deepStrictEqual(registry.getExtensions('a'), []); + assert.deepStrictEqual(registry.getExtensions('aname'), []); + assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt']); registry._registerLanguages([{ id: 'a', extensions: ['aExt2'] }]); - assert.deepEqual(registry.getExtensions('a'), []); - assert.deepEqual(registry.getExtensions('aname'), []); - assert.deepEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); + assert.deepStrictEqual(registry.getExtensions('a'), []); + assert.deepStrictEqual(registry.getExtensions('aname'), []); + assert.deepStrictEqual(registry.getExtensions('aName'), ['aExt', 'aExt2']); }); test('extensions of primary language registration come first', () => { @@ -229,7 +229,7 @@ suite('LanguagesRegistry', () => { extensions: ['aExt3'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt3'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt3'); registry._registerLanguages([{ id: 'a', @@ -237,14 +237,14 @@ suite('LanguagesRegistry', () => { extensions: ['aExt'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); registry._registerLanguages([{ id: 'a', extensions: ['aExt2'] }]); - assert.deepEqual(registry.getExtensions('a')[0], 'aExt'); + assert.deepStrictEqual(registry.getExtensions('a')[0], 'aExt'); }); test('filenames', () => { @@ -256,18 +256,18 @@ suite('LanguagesRegistry', () => { filenames: ['aFilename'] }]); - assert.deepEqual(registry.getFilenames('a'), []); - assert.deepEqual(registry.getFilenames('aname'), []); - assert.deepEqual(registry.getFilenames('aName'), ['aFilename']); + assert.deepStrictEqual(registry.getFilenames('a'), []); + assert.deepStrictEqual(registry.getFilenames('aname'), []); + assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename']); registry._registerLanguages([{ id: 'a', filenames: ['aFilename2'] }]); - assert.deepEqual(registry.getFilenames('a'), []); - assert.deepEqual(registry.getFilenames('aname'), []); - assert.deepEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); + assert.deepStrictEqual(registry.getFilenames('a'), []); + assert.deepStrictEqual(registry.getFilenames('aname'), []); + assert.deepStrictEqual(registry.getFilenames('aName'), ['aFilename', 'aFilename2']); }); test('configuration', () => { @@ -279,17 +279,17 @@ suite('LanguagesRegistry', () => { configuration: URI.file('/path/to/aFilename') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); - assert.deepEqual(registry.getConfigurationFiles('aname'), []); - assert.deepEqual(registry.getConfigurationFiles('aName'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename')]); + assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); registry._registerLanguages([{ id: 'a', configuration: URI.file('/path/to/aFilename2') }]); - assert.deepEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); - assert.deepEqual(registry.getConfigurationFiles('aname'), []); - assert.deepEqual(registry.getConfigurationFiles('aName'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('a'), [URI.file('/path/to/aFilename'), URI.file('/path/to/aFilename2')]); + assert.deepStrictEqual(registry.getConfigurationFiles('aname'), []); + assert.deepStrictEqual(registry.getConfigurationFiles('aName'), []); }); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index c759a186ab..70ad341bb5 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -11,18 +11,26 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { DefaultEndOfLine, ITextModel } from 'vs/editor/common/model'; import { createTextBuffer } from 'vs/editor/common/model/textModel'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ModelSemanticColoring, ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DocumentSemanticTokensProvider, DocumentSemanticTokensProviderRegistry, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/modes'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Barrier, timeout } from 'vs/base/common/async'; +import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; const GENERATE_TESTS = false; @@ -47,9 +55,9 @@ suite('ModelService', () => { const model2 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\myroot\\myfile.txt' : '/myroot/myfile.txt')); const model3 = modelService.createModel('farboo', null, URI.file(platform.isWindows ? 'c:\\other\\myfile.txt' : '/other/myfile.txt')); - assert.equal(model1.getOptions().defaultEOL, DefaultEndOfLine.LF); - assert.equal(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); - assert.equal(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); + assert.strictEqual(model1.getOptions().defaultEOL, DefaultEndOfLine.LF); + assert.strictEqual(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); + assert.strictEqual(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); }); test('_computeEdits no change', function () { @@ -71,11 +79,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, []); + assert.deepStrictEqual(actual, []); }); test('_computeEdits first line changed', function () { @@ -97,11 +105,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove(new Range(1, 1, 2, 1), 'This is line One\n') ]); }); @@ -125,11 +133,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, []); + assert.deepStrictEqual(actual, []); }); test('_computeEdits EOL and other change 1', function () { @@ -151,11 +159,11 @@ suite('ModelService', () => { 'and finished with the fourth.', //29 ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove( new Range(1, 1, 4, 1), [ @@ -186,11 +194,11 @@ suite('ModelService', () => { '' ].join('\r\n'), DefaultEndOfLine.LF - ); + ).textBuffer; const actual = ModelServiceImpl._computeEdits(model, textBuffer); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ EditOperation.replaceMove(new Range(3, 2, 3, 2), '\r\n') ]); }); @@ -317,7 +325,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); // dispose it modelService.destroyModel(resource); @@ -325,7 +333,7 @@ suite('ModelService', () => { const model2 = modelService.createModel('text1', null, resource); // undo model2.undo(); - assert.equal(model2.getValue(), 'text'); + assert.strictEqual(model2.getValue(), 'text'); }); test('maintains version id and alternative version id for same resource and same content', () => { @@ -335,7 +343,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); const versionId = model1.getVersionId(); const alternativeVersionId = model1.getAlternativeVersionId(); // dispose it @@ -343,8 +351,8 @@ suite('ModelService', () => { // create a new model with the same content const model2 = modelService.createModel('text1', null, resource); - assert.equal(model2.getVersionId(), versionId); - assert.equal(model2.getAlternativeVersionId(), alternativeVersionId); + assert.strictEqual(model2.getVersionId(), versionId); + assert.strictEqual(model2.getAlternativeVersionId(), alternativeVersionId); }); test('does not maintain undo for same resource and different content', () => { @@ -354,7 +362,7 @@ suite('ModelService', () => { const model1 = modelService.createModel('text', null, resource); // make an edit model1.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model1.getValue(), 'text1'); + assert.strictEqual(model1.getValue(), 'text1'); // dispose it modelService.destroyModel(resource); @@ -362,7 +370,7 @@ suite('ModelService', () => { const model2 = modelService.createModel('text2', null, resource); // undo model2.undo(); - assert.equal(model2.getValue(), 'text2'); + assert.strictEqual(model2.getValue(), 'text2'); }); test('setValue should clear undo stack', () => { @@ -370,17 +378,98 @@ suite('ModelService', () => { const model = modelService.createModel('text', null, resource); model.pushEditOperations(null, [{ range: new Range(1, 5, 1, 5), text: '1' }], () => [new Selection(1, 5, 1, 5)]); - assert.equal(model.getValue(), 'text1'); + assert.strictEqual(model.getValue(), 'text1'); model.setValue('text2'); model.undo(); - assert.equal(model.getValue(), 'text2'); + assert.strictEqual(model.getValue(), 'text2'); + }); +}); + +suite('ModelSemanticColoring', () => { + + const disposables = new DisposableStore(); + const ORIGINAL_FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY; + let modelService: IModelService; + let modeService: IModeService; + + setup(() => { + ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = 0; + + const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } }); + const themeService = new TestThemeService(); + themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true)); + modelService = disposables.add(new ModelServiceImpl( + configService, + new TestTextResourcePropertiesService(configService), + themeService, + new NullLogService(), + new UndoRedoService(new TestDialogService(), new TestNotificationService()) + )); + modeService = disposables.add(new ModeServiceImpl(false)); + }); + + teardown(() => { + disposables.clear(); + ModelSemanticColoring.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY = ORIGINAL_FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY; + }); + + test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => { + + disposables.add(ModesRegistry.registerLanguage({ id: 'testMode' })); + + const inFirstCall = new Barrier(); + const delayFirstResult = new Barrier(); + const secondResultProvided = new Barrier(); + let callCount = 0; + + disposables.add(DocumentSemanticTokensProviderRegistry.register('testMode', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + if (callCount === 1) { + assert.ok('called once'); + inFirstCall.open(); + await delayFirstResult.wait(); + await timeout(0); // wait for the simple scheduler to fire to check that we do actually get rescheduled + return null; + } + if (callCount === 2) { + assert.ok('called twice'); + secondResultProvided.open(); + return null; + } + assert.fail('Unexpected call'); + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + const textModel = disposables.add(modelService.createModel('Hello world', modeService.create('testMode'))); + + // wait for the provider to be called + await inFirstCall.wait(); + + // the provider is now in the provide call + // change the text buffer while the provider is running + textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }]); + + // let the provider finish its first result + delayFirstResult.open(); + + // we need to check that the provider is called again, even if it returns null + await secondResultProvided.wait(); + + // assert that it got called twice + assert.strictEqual(callCount, 2); }); }); function assertComputeEdits(lines1: string[], lines2: string[]): void { const model = createTextModel(lines1.join('\n')); - const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF); + const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF).textBuffer; // compute required edits // let start = Date.now(); @@ -390,7 +479,7 @@ function assertComputeEdits(lines1: string[], lines2: string[]): void { // apply edits model.pushEditOperations([], edits, null); - assert.equal(model.getValue(), lines2.join('\n')); + assert.strictEqual(model.getValue(), lines2.join('\n')); } function getRandomInt(min: number, max: number): number { @@ -439,21 +528,3 @@ assertComputeEdits(file1, file2); } } } - -export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { - - declare readonly _serviceBrand: undefined; - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - } - - getEOL(resource: URI, language?: string): string { - const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource }); - if (eol && eol !== 'auto') { - return eol; - } - return (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n'; - } -} diff --git a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts b/src/vs/editor/test/common/services/semanticTokensDto.test.ts similarity index 95% rename from src/vs/workbench/test/common/api/semanticTokensDto.test.ts rename to src/vs/editor/test/common/services/semanticTokensDto.test.ts index e1155c4fb7..b2fc9fb185 100644 --- a/src/vs/workbench/test/common/api/semanticTokensDto.test.ts +++ b/src/vs/editor/test/common/services/semanticTokensDto.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { IFullSemanticTokensDto, IDeltaSemanticTokensDto, encodeSemanticTokensDto, ISemanticTokensDto, decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { VSBuffer } from 'vs/base/common/buffer'; suite('SemanticTokensDto', () => { @@ -25,7 +25,7 @@ suite('SemanticTokensDto', () => { data: toArr(dto.data) }; }; - assert.deepEqual(convert(actual), convert(expected)); + assert.deepStrictEqual(convert(actual), convert(expected)); } function assertEqualDelta(actual: IDeltaSemanticTokensDto, expected: IDeltaSemanticTokensDto): void { @@ -46,7 +46,7 @@ suite('SemanticTokensDto', () => { deltas: dto.deltas.map(convertOne) }; }; - assert.deepEqual(convert(actual), convert(expected)); + assert.deepStrictEqual(convert(actual), convert(expected)); } function testRoundTrip(value: ISemanticTokensDto): void { diff --git a/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts b/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts new file mode 100644 index 0000000000..b112e11e77 --- /dev/null +++ b/src/vs/editor/test/common/services/testTextResourcePropertiesService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + } + + getEOL(resource: URI, language?: string): string { + const eol = this.configurationService.getValue('files.eol', { overrideIdentifier: language, resource }); + if (eol && eol !== 'auto') { + return eol; + } + return (platform.isLinux || platform.isMacintosh) ? '\n' : '\r\n'; + } +} diff --git a/src/vs/editor/test/common/view/overviewZoneManager.test.ts b/src/vs/editor/test/common/view/overviewZoneManager.test.ts index 689227aea7..823dfc8308 100644 --- a/src/vs/editor/test/common/view/overviewZoneManager.test.ts +++ b/src/vs/editor/test/common/view/overviewZoneManager.test.ts @@ -26,7 +26,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 12, but cap is at 6 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(12, 24, 1), // new ColorZone(120, 132, 2), // 120 -> 132 new ColorZone(360, 384, 3), // 360 -> 372 [360 -> 384] @@ -52,7 +52,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 6, cap is at 6 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(6, 12, 1), // new ColorZone(60, 66, 2), // 60 -> 66 new ColorZone(180, 192, 3), // 180 -> 192 @@ -78,7 +78,7 @@ suite('Editor View - OverviewZoneManager', () => { ]); // one line = 6, cap is at 12 - assert.deepEqual(manager.resolveColorZones(), [ + assert.deepStrictEqual(manager.resolveColorZones(), [ new ColorZone(12, 24, 1), // new ColorZone(120, 132, 2), // 120 -> 132 new ColorZone(360, 384, 3), // 360 -> 384 diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index 279580b794..9c18eb893c 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -79,7 +79,8 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { options._write(EditorOption.wordWrap, 'off'); options._write(EditorOption.wordWrapColumn, 80); - options._write(EditorOption.wordWrapMinified, true); + options._write(EditorOption.wordWrapOverride1, 'inherit'); + options._write(EditorOption.wordWrapOverride2, 'inherit'); options._write(EditorOption.accessibilitySupport, 'auto'); const actual = EditorLayoutInfoComputer.computeLayout(options, { @@ -94,7 +95,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { maxDigitWidth: input.maxDigitWidth, pixelRatio: input.pixelRatio, }); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } test('EditorLayoutProvider 1', () => { diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index 1876d0167d..53b9907798 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -17,7 +17,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new DecorationSegment(0, 1, 'c1', 0), new DecorationSegment(2, 2, 'c2 c1', 0), new DecorationSegment(3, 9, 'c1', 0), @@ -31,7 +31,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(20, 21, 'inline-folded', InlineDecorationType.Regular), ]); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new DecorationSegment(14, 18, 'mtkw', 0), new DecorationSegment(19, 19, 'mtkw inline-folded', 0) ]); @@ -43,7 +43,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new InlineDecoration(new Range(2, 12, 3, 30), 'detected-link', InlineDecorationType.Regular) ], 3, 12, 500); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new LineDecoration(12, 30, 'detected-link', InlineDecorationType.Regular), ]); }); @@ -54,7 +54,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new InlineDecoration(new Range(4, 0, 4, 1), 'after', InlineDecorationType.After), ], 4, 1, 500); - assert.deepEqual(result, [ + assert.deepStrictEqual(result, [ new LineDecoration(1, 2, 'before', InlineDecorationType.Before), new LineDecoration(0, 1, 'after', InlineDecorationType.After), ]); @@ -62,7 +62,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { test('ViewLineParts', () => { - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 2, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -70,7 +70,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 3, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -78,7 +78,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ @@ -86,7 +86,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) @@ -95,7 +95,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), @@ -105,7 +105,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c1** c2', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), @@ -116,7 +116,7 @@ suite('Editor ViewLayout - ViewLineParts', () => { new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*', 0) ]); - assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ + assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 422277010e..10d83a7a9d 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -34,105 +34,105 @@ suite('Editor ViewLayout - LinesLayout', () => { // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - - assert.equal(linesLayout.getLinesTotalHeight(), 100); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 60); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 70); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 80); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 90); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 100); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 60); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 70); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 80); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 90); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); // Add whitespace of height 5px after 2nd line insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 105); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 35); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 45); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 105); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 35); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 45); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(45), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(104), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(45), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(104), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); // Add two more whitespaces of height 5px insertWhitespace(linesLayout, 3, 0, 5, 0); insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) - assert.equal(linesLayout.getLinesTotalHeight(), 115); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 65); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 115); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 65); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 6); - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 20); // 20 -> 25 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 35); // 35 -> 40 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 20); // 20 -> 25 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 35); // 35 -> 40 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 50); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(19), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(20), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(21), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(22), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(23), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(24), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(25), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(26), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(36), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(41), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(51), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), -1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(1000), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(19), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(20), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(21), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(22), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(23), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(24), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(25), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(26), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(36), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(41), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(51), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(1000), -1); }); @@ -144,94 +144,94 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 7); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 8); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 9); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 11); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 12); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 13); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 14); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 7); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 8); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 9); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 11); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 12); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 13); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 14); // Change whitespace height // 10 lines // whitespace: - a(2,10) changeOneWhitespace(linesLayout, a, 2, 10); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 12); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 13); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 14); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 12); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 13); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 14); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Change whitespace position // 10 lines // whitespace: - a(5,10) changeOneWhitespace(linesLayout, a, 5, 10); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Pretend that lines 5 and 6 were deleted // 8 lines // whitespace: - a(4,10) linesLayout.onLinesDeleted(5, 6); - assert.equal(linesLayout.getLinesTotalHeight(), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 14); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 14); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); // Insert two lines at the beginning // 10 lines // whitespace: - a(6,10) linesLayout.onLinesInserted(1, 2); - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Remove whitespace // 10 lines removeWhitespace(linesLayout, a); - assert.equal(linesLayout.getLinesTotalHeight(), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 6); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 7); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 8); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 9); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 6); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 7); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 8); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 9); }); test('LinesLayout Padding', () => { @@ -240,93 +240,93 @@ suite('Editor ViewLayout - LinesLayout', () => { // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: - - assert.equal(linesLayout.getLinesTotalHeight(), 135); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 35); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 45); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 65); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 75); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 85); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 95); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 105); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 135); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 35); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 45); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 65); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 75); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 85); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 95); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 105); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); // Add whitespace of height 5px after 2nd line insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) - assert.equal(linesLayout.getLinesTotalHeight(), 140); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 50); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 140); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 50); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(41), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(25), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(34), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(41), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); // Add two more whitespaces of height 5px insertWhitespace(linesLayout, 3, 0, 5, 0); insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) - assert.equal(linesLayout.getLinesTotalHeight(), 150); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 15); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 25); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 55); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 70); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 80); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 150); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 15); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 25); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 55); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 70); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 80); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(30), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(54), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(55), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(69), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(70), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(80), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(24), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(30), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(35), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(39), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(40), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(49), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(50), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(54), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(55), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(64), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(65), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(69), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(70), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(80), 6); - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 35); // 35 -> 40 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 50); // 50 -> 55 - assert.equal(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 65); + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(0), 35); // 35 -> 40 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(1), 50); // 50 -> 55 + assert.strictEqual(linesLayout.getVerticalOffsetForWhitespaceIndex(2), 65); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 0); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 1); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(64), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(65), 2); - assert.equal(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(70), -1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(0), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(34), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(35), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(39), 0); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(40), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(49), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(50), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(54), 1); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(55), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(64), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(65), 2); + assert.strictEqual(linesLayout.getWhitespaceIndexAtOrAfterVerticallOffset(70), -1); }); test('LinesLayout getLineNumberAtOrAfterVerticalOffset', () => { @@ -335,47 +335,47 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,10) - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Do some hit testing // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(-100), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(-1), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 2); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(2), 3); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(3), 4); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(4), 5); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 6); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(6), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(7), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(8), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(12), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(13), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(14), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(16), 7); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(17), 8); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(18), 9); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(22), 10); - assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(23), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(-100), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(-1), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(0), 1); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(1), 2); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(2), 3); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(3), 4); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(4), 5); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(5), 6); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(6), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(7), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(8), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(9), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(10), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(11), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(12), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(13), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(14), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(15), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(16), 7); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(17), 8); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(18), 9); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(19), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(20), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(21), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(22), 10); + assert.strictEqual(linesLayout.getLineNumberAtOrAfterVerticalOffset(23), 10); }); test('LinesLayout getCenteredLineInViewport', () => { @@ -384,81 +384,81 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,10) - assert.equal(linesLayout.getLinesTotalHeight(), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 2); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 3); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 4); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 5); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 16); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 17); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 18); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 19); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 2); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 3); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 4); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 5); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 16); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 17); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 18); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 19); // Find centered line in viewport 1 // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLinesViewportData(0, 1).centeredLineNumber, 1); - assert.equal(linesLayout.getLinesViewportData(0, 2).centeredLineNumber, 2); - assert.equal(linesLayout.getLinesViewportData(0, 3).centeredLineNumber, 2); - assert.equal(linesLayout.getLinesViewportData(0, 4).centeredLineNumber, 3); - assert.equal(linesLayout.getLinesViewportData(0, 5).centeredLineNumber, 3); - assert.equal(linesLayout.getLinesViewportData(0, 6).centeredLineNumber, 4); - assert.equal(linesLayout.getLinesViewportData(0, 7).centeredLineNumber, 4); - assert.equal(linesLayout.getLinesViewportData(0, 8).centeredLineNumber, 5); - assert.equal(linesLayout.getLinesViewportData(0, 9).centeredLineNumber, 5); - assert.equal(linesLayout.getLinesViewportData(0, 10).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 11).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 12).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 13).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 14).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 15).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 16).centeredLineNumber, 6); - assert.equal(linesLayout.getLinesViewportData(0, 17).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 18).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 19).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 21).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 22).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 23).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 24).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 25).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 26).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 27).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 28).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 29).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 30).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 31).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 32).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(0, 33).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 1).centeredLineNumber, 1); + assert.strictEqual(linesLayout.getLinesViewportData(0, 2).centeredLineNumber, 2); + assert.strictEqual(linesLayout.getLinesViewportData(0, 3).centeredLineNumber, 2); + assert.strictEqual(linesLayout.getLinesViewportData(0, 4).centeredLineNumber, 3); + assert.strictEqual(linesLayout.getLinesViewportData(0, 5).centeredLineNumber, 3); + assert.strictEqual(linesLayout.getLinesViewportData(0, 6).centeredLineNumber, 4); + assert.strictEqual(linesLayout.getLinesViewportData(0, 7).centeredLineNumber, 4); + assert.strictEqual(linesLayout.getLinesViewportData(0, 8).centeredLineNumber, 5); + assert.strictEqual(linesLayout.getLinesViewportData(0, 9).centeredLineNumber, 5); + assert.strictEqual(linesLayout.getLinesViewportData(0, 10).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 11).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 12).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 13).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 14).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 15).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 16).centeredLineNumber, 6); + assert.strictEqual(linesLayout.getLinesViewportData(0, 17).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 18).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 19).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 21).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 22).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 23).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 24).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 25).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 26).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 27).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 28).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 29).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 30).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 31).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 32).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(0, 33).centeredLineNumber, 7); // Find centered line in viewport 2 // line [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // vertical: [0, 1, 2, 3, 4, 5, 16, 17, 18, 19] - assert.equal(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(1, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(2, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(3, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(4, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(5, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(6, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(7, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(8, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(9, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(10, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(11, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(12, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(13, 20).centeredLineNumber, 7); - assert.equal(linesLayout.getLinesViewportData(14, 20).centeredLineNumber, 8); - assert.equal(linesLayout.getLinesViewportData(15, 20).centeredLineNumber, 8); - assert.equal(linesLayout.getLinesViewportData(16, 20).centeredLineNumber, 9); - assert.equal(linesLayout.getLinesViewportData(17, 20).centeredLineNumber, 9); - assert.equal(linesLayout.getLinesViewportData(18, 20).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(19, 20).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(20, 23).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(21, 23).centeredLineNumber, 10); - assert.equal(linesLayout.getLinesViewportData(22, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(0, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(1, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(2, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(3, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(4, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(5, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(6, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(7, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(8, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(9, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(10, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(11, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(12, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(13, 20).centeredLineNumber, 7); + assert.strictEqual(linesLayout.getLinesViewportData(14, 20).centeredLineNumber, 8); + assert.strictEqual(linesLayout.getLinesViewportData(15, 20).centeredLineNumber, 8); + assert.strictEqual(linesLayout.getLinesViewportData(16, 20).centeredLineNumber, 9); + assert.strictEqual(linesLayout.getLinesViewportData(17, 20).centeredLineNumber, 9); + assert.strictEqual(linesLayout.getLinesViewportData(18, 20).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(19, 20).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(20, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(21, 23).centeredLineNumber, 10); + assert.strictEqual(linesLayout.getLinesViewportData(22, 23).centeredLineNumber, 10); }); test('LinesLayout getLinesViewportData 1', () => { @@ -467,131 +467,131 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,100) - assert.equal(linesLayout.getLinesTotalHeight(), 200); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 160); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 170); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 180); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 190); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 200); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 160); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 170); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 180); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 190); // viewport 0->50 let viewportData = linesLayout.getLinesViewportData(0, 50); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 5); - assert.equal(viewportData.completelyVisibleStartLineNumber, 1); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 5); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 1); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40]); // viewport 1->51 viewportData = linesLayout.getLinesViewportData(1, 51); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); // viewport 5->55 viewportData = linesLayout.getLinesViewportData(5, 55); - assert.equal(viewportData.startLineNumber, 1); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 5); - assert.deepEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 1); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 5); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [0, 10, 20, 30, 40, 50]); // viewport 10->60 viewportData = linesLayout.getLinesViewportData(10, 60); - assert.equal(viewportData.startLineNumber, 2); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 2); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [10, 20, 30, 40, 50]); + assert.strictEqual(viewportData.startLineNumber, 2); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 2); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [10, 20, 30, 40, 50]); // viewport 50->100 viewportData = linesLayout.getLinesViewportData(50, 100); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 60->110 viewportData = linesLayout.getLinesViewportData(60, 110); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 65->115 viewportData = linesLayout.getLinesViewportData(65, 115); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 50->159 viewportData = linesLayout.getLinesViewportData(50, 159); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 50->160 viewportData = linesLayout.getLinesViewportData(50, 160); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); // viewport 51->161 viewportData = linesLayout.getLinesViewportData(51, 161); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); // viewport 150->169 viewportData = linesLayout.getLinesViewportData(150, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 159->169 viewportData = linesLayout.getLinesViewportData(159, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 160->169 viewportData = linesLayout.getLinesViewportData(160, 169); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [160]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160]); // viewport 160->1000 viewportData = linesLayout.getLinesViewportData(160, 1000); - assert.equal(viewportData.startLineNumber, 7); - assert.equal(viewportData.endLineNumber, 10); - assert.equal(viewportData.completelyVisibleStartLineNumber, 7); - assert.equal(viewportData.completelyVisibleEndLineNumber, 10); - assert.deepEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); + assert.strictEqual(viewportData.startLineNumber, 7); + assert.strictEqual(viewportData.endLineNumber, 10); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 10); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); }); test('LinesLayout getLinesViewportData 2 & getWhitespaceViewportData', () => { @@ -601,27 +601,27 @@ suite('Editor ViewLayout - LinesLayout', () => { // 10 lines // whitespace: - a(6,100), b(7, 50) - assert.equal(linesLayout.getLinesTotalHeight(), 250); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 10); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(3), 20); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(4), 30); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(5), 40); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(6), 50); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(7), 160); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(8), 220); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(9), 230); - assert.equal(linesLayout.getVerticalOffsetForLineNumber(10), 240); + assert.strictEqual(linesLayout.getLinesTotalHeight(), 250); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 10); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(3), 20); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(4), 30); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(5), 40); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(6), 50); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(7), 160); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(8), 220); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(9), 230); + assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(10), 240); // viewport 50->160 let viewportData = linesLayout.getLinesViewportData(50, 160); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 6); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 6); - assert.deepEqual(viewportData.relativeVerticalOffset, [50]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 6); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50]); let whitespaceData = linesLayout.getWhitespaceViewportData(50, 160); - assert.deepEqual(whitespaceData, [{ + assert.deepStrictEqual(whitespaceData, [{ id: a, afterLineNumber: 6, verticalOffset: 60, @@ -630,13 +630,13 @@ suite('Editor ViewLayout - LinesLayout', () => { // viewport 50->219 viewportData = linesLayout.getLinesViewportData(50, 219); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); whitespaceData = linesLayout.getWhitespaceViewportData(50, 219); - assert.deepEqual(whitespaceData, [{ + assert.deepStrictEqual(whitespaceData, [{ id: a, afterLineNumber: 6, verticalOffset: 60, @@ -650,19 +650,19 @@ suite('Editor ViewLayout - LinesLayout', () => { // viewport 50->220 viewportData = linesLayout.getLinesViewportData(50, 220); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 7); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 7); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 7); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 7); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160]); // viewport 50->250 viewportData = linesLayout.getLinesViewportData(50, 250); - assert.equal(viewportData.startLineNumber, 6); - assert.equal(viewportData.endLineNumber, 10); - assert.equal(viewportData.completelyVisibleStartLineNumber, 6); - assert.equal(viewportData.completelyVisibleEndLineNumber, 10); - assert.deepEqual(viewportData.relativeVerticalOffset, [50, 160, 220, 230, 240]); + assert.strictEqual(viewportData.startLineNumber, 6); + assert.strictEqual(viewportData.endLineNumber, 10); + assert.strictEqual(viewportData.completelyVisibleStartLineNumber, 6); + assert.strictEqual(viewportData.completelyVisibleEndLineNumber, 10); + assert.deepStrictEqual(viewportData.relativeVerticalOffset, [50, 160, 220, 230, 240]); }); test('LinesLayout getWhitespaceAtVerticalOffset', () => { @@ -671,40 +671,40 @@ suite('Editor ViewLayout - LinesLayout', () => { let b = insertWhitespace(linesLayout, 7, 0, 50, 0); let whitespace = linesLayout.getWhitespaceAtVerticalOffset(0); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(59); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(60); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(61); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(159); - assert.equal(whitespace!.id, a); + assert.strictEqual(whitespace!.id, a); whitespace = linesLayout.getWhitespaceAtVerticalOffset(160); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(161); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(169); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); whitespace = linesLayout.getWhitespaceAtVerticalOffset(170); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(171); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(219); - assert.equal(whitespace!.id, b); + assert.strictEqual(whitespace!.id, b); whitespace = linesLayout.getWhitespaceAtVerticalOffset(220); - assert.equal(whitespace, null); + assert.strictEqual(whitespace, null); }); test('LinesLayout', () => { @@ -714,230 +714,230 @@ suite('Editor ViewLayout - LinesLayout', () => { // Insert a whitespace after line number 2, of height 10 const a = insertWhitespace(linesLayout, 2, 0, 10, 0); // whitespaces: a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 1); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + assert.strictEqual(linesLayout.getWhitespacesCount(), 1); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); // Insert a whitespace again after line number 2, of height 20 let b = insertWhitespace(linesLayout, 2, 0, 20, 0); // whitespaces: a(2, 10), b(2, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 30); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 30); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); // Change last inserted whitespace height to 30 changeOneWhitespace(linesLayout, b, 2, 30); // whitespaces: a(2, 10), b(2, 30) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 40); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 40); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 40); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 40); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); // Remove last inserted whitespace removeWhitespace(linesLayout, b); // whitespaces: a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 1); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + assert.strictEqual(linesLayout.getWhitespacesCount(), 1); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); // Add a whitespace before the first line of height 50 b = insertWhitespace(linesLayout, 0, 0, 50, 0); // whitespaces: b(0, 50), a(2, 10) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); // Add a whitespace after line 4 of height 20 insertWhitespace(linesLayout, 4, 0, 20, 0); // whitespaces: b(0, 50), a(2, 10), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 3); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 80); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 80); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); + assert.strictEqual(linesLayout.getWhitespacesCount(), 3); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 80); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 80); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); // Add a whitespace after line 3 of height 30 insertWhitespace(linesLayout, 3, 0, 30, 0); // whitespaces: b(0, 50), a(2, 10), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 4); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 90); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 110); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 110); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); + assert.strictEqual(linesLayout.getWhitespacesCount(), 4); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 90); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(3), 110); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 110); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); // Change whitespace after line 2 to height of 100 changeOneWhitespace(linesLayout, a, 2, 100); // whitespaces: b(0, 50), a(2, 100), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 4); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 100); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 150); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 180); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 200); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 200); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); + assert.strictEqual(linesLayout.getWhitespacesCount(), 4); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 100); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 150); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 180); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(3), 200); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 200); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); // Remove whitespace after line 2 removeWhitespace(linesLayout, a); // whitespaces: b(0, 50), d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 3); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 80); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 100); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 100); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); + assert.strictEqual(linesLayout.getWhitespacesCount(), 3); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 80); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(2), 100); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 100); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); // Remove whitespace before line 1 removeWhitespace(linesLayout, b); // whitespaces: d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Delete line 1 linesLayout.onLinesDeleted(1, 1); // whitespaces: d(2, 30), c(3, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Insert a line before line 1 linesLayout.onLinesInserted(1, 1); // whitespaces: d(3, 30), c(4, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Delete line 4 linesLayout.onLinesDeleted(4, 4); // whitespaces: d(3, 30), c(3, 20) - assert.equal(linesLayout.getWhitespacesCount(), 2); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); - assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); - assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + assert.strictEqual(linesLayout.getWhitespacesCount(), 2); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.strictEqual(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.strictEqual(linesLayout.getWhitespacesTotalHeight(), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); }); test('LinesLayout findInsertionIndex', () => { @@ -949,114 +949,114 @@ suite('Editor ViewLayout - LinesLayout', () => { let arr: EditorWhitespace[]; arr = makeInternalWhitespace([]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 0); arr = makeInternalWhitespace([1]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); arr = makeInternalWhitespace([1, 3]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); arr = makeInternalWhitespace([1, 3, 5]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); arr = makeInternalWhitespace([1, 3, 5], 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); arr = makeInternalWhitespace([1, 3, 5, 7]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); arr = makeInternalWhitespace([1, 3, 5, 7, 9]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 14, 0), 7); arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13, 15]); - assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); - assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); - assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); - assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); - assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); - assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); - assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); - assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); - assert.equal(LinesLayout.findInsertionIndex(arr, 15, 0), 8); - assert.equal(LinesLayout.findInsertionIndex(arr, 16, 0), 8); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 15, 0), 8); + assert.strictEqual(LinesLayout.findInsertionIndex(arr, 16, 0), 8); }); test('LinesLayout changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { @@ -1066,121 +1066,121 @@ suite('Editor ViewLayout - LinesLayout', () => { const b = insertWhitespace(linesLayout, 7, 0, 1, 0); const c = insertWhitespace(linesLayout, 3, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Do not really move a changeOneWhitespace(linesLayout, a, 1, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 1 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 1 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Do not really move a changeOneWhitespace(linesLayout, a, 2, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 2 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 2 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Change a to conflict with c => a gets placed after c changeOneWhitespace(linesLayout, a, 3, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Make a no-op changeOneWhitespace(linesLayout, c, 3, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- // Conflict c with b => c gets placed after b changeOneWhitespace(linesLayout, c, 7, 1); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 3 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 7 - assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 3 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 7 + assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b - assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b + assert.strictEqual(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- }); test('LinesLayout Bug', () => { @@ -1189,53 +1189,53 @@ suite('Editor ViewLayout - LinesLayout', () => { const a = insertWhitespace(linesLayout, 0, 0, 1, 0); const b = insertWhitespace(linesLayout, 7, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), b); // 7 const c = insertWhitespace(linesLayout, 3, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), b); // 7 const d = insertWhitespace(linesLayout, 2, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 const e = insertWhitespace(linesLayout, 8, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 const f = insertWhitespace(linesLayout, 11, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), f); // 11 const g = insertWhitespace(linesLayout, 10, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), g); // 10 - assert.equal(linesLayout.getIdForWhitespaceIndex(6), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), g); // 10 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(6), f); // 11 const h = insertWhitespace(linesLayout, 0, 0, 1, 0); - assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(1), h); // 0 - assert.equal(linesLayout.getIdForWhitespaceIndex(2), d); // 2 - assert.equal(linesLayout.getIdForWhitespaceIndex(3), c); // 3 - assert.equal(linesLayout.getIdForWhitespaceIndex(4), b); // 7 - assert.equal(linesLayout.getIdForWhitespaceIndex(5), e); // 8 - assert.equal(linesLayout.getIdForWhitespaceIndex(6), g); // 10 - assert.equal(linesLayout.getIdForWhitespaceIndex(7), f); // 11 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(1), h); // 0 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(2), d); // 2 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(3), c); // 3 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(4), b); // 7 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(5), e); // 8 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(6), g); // 10 + assert.strictEqual(linesLayout.getIdForWhitespaceIndex(7), f); // 11 }); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index b066e51326..e21775adc0 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -48,7 +48,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expected + ''); + assert.strictEqual(_actual.html, '' + expected + ''); assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); } @@ -101,7 +101,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expected + ''); + assert.strictEqual(_actual.html, '' + expected + ''); assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart, expectedPartLengts); } @@ -167,7 +167,7 @@ suite('viewLineRenderer.renderLine', () => { '' ].join(''); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, [ [0], @@ -252,7 +252,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [4, 4, 6, 1, 5, 1, 4, 1, 1, 1, 3, 15, 2, 3]); }); @@ -318,7 +318,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); @@ -384,7 +384,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); @@ -444,7 +444,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(actual.html, '' + expectedOutput + ''); + assert.strictEqual(actual.html, '' + expectedOutput + ''); assertCharacterMapping2(actual.characterMapping, expectedCharacterMapping); }); @@ -487,8 +487,8 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); - assert.equal(_actual.containsRTL, true); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.containsRTL, true); }); test('issue #6885: Splits large tokens', () => { @@ -520,7 +520,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + '', message); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + '', message); } // A token with 49 chars @@ -624,7 +624,7 @@ suite('viewLineRenderer.renderLine', () => { true, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + '', message); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + '', message); } // A token with 101 chars @@ -669,7 +669,7 @@ suite('viewLineRenderer.renderLine', () => { let expectedOutput = [ 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷', ]; - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); }); test('issue #6885: Does not split large tokens in RTL text', () => { @@ -699,8 +699,8 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); - assert.equal(actual.containsRTL, true); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.containsRTL, true); }); test('issue #95685: Uses unicode replacement character for Paragraph Separator', () => { @@ -730,7 +730,7 @@ suite('viewLineRenderer.renderLine', () => { false, null )); - assert.equal(actual.html, '' + expectedOutput.join('') + ''); + assert.strictEqual(actual.html, '' + expectedOutput.join('') + ''); }); test('issue #19673: Monokai Theme bad-highlighting in line wrap', () => { @@ -780,7 +780,7 @@ suite('viewLineRenderer.renderLine', () => { null )); - assert.equal(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.html, '' + expectedOutput + ''); }); interface ICharMappingData { @@ -807,7 +807,7 @@ suite('viewLineRenderer.renderLine', () => { function assertCharacterMapping2(actual: CharacterMapping, expected: CharacterMapping): void { const _actual = decodeCharacterMapping(actual); const _expected = decodeCharacterMapping(expected); - assert.deepEqual(_actual, _expected); + assert.deepStrictEqual(_actual, _expected); } function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { @@ -830,7 +830,7 @@ suite('viewLineRenderer.renderLine', () => { for (let i = 0; i < tmp.length; i++) { actualCharOffset[i] = tmp[i]; } - assert.deepEqual(actualCharOffset, expectedCharAbsoluteOffset); + assert.deepStrictEqual(actualCharOffset, expectedCharAbsoluteOffset); } function assertCharPartOffsets(actual: CharacterMapping, expected: number[][]): void { @@ -844,7 +844,7 @@ suite('viewLineRenderer.renderLine', () => { let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); - assert.deepEqual( + assert.deepStrictEqual( { partIndex: actualPartIndex, charIndex: actualCharIndex }, { partIndex: partIndex, charIndex: charIndex }, `character mapping for offset ${charOffset}` @@ -853,7 +853,7 @@ suite('viewLineRenderer.renderLine', () => { // here let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); - assert.equal( + assert.strictEqual( actualOffset, charOffset, `character mapping for part ${partIndex}, ${charIndex}` @@ -863,7 +863,7 @@ suite('viewLineRenderer.renderLine', () => { } } - assert.equal(actual.length, charOffset); + assert.strictEqual(actual.length, charOffset); } }); @@ -892,7 +892,7 @@ suite('viewLineRenderer.renderLine 2', () => { selections )); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); } test('issue #18616: Inline decorations ending at the text length are no longer rendered', () => { @@ -927,7 +927,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #19207: Link in Monokai is not rendered correctly', () => { @@ -976,7 +976,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('createLineParts simple', () => { @@ -1477,7 +1477,7 @@ suite('viewLineRenderer.renderLine 2', () => { // bb--------- // -cccccc---- - assert.deepEqual(actual.html, [ + assert.deepStrictEqual(actual.html, [ '', 'H', 'e', @@ -1522,7 +1522,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #32436: Non-monospace font + visible whitespace + After decorator causes line to "jump"', () => { @@ -1559,7 +1559,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #30133: Empty lines don\'t render inline decorations', () => { @@ -1594,7 +1594,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', () => { @@ -1628,7 +1628,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #37401 #40127: Allow both before and after decorations on empty line', () => { @@ -1665,7 +1665,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #38935: GitLens end-of-line blame no longer rendering', () => { @@ -1702,7 +1702,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22832: Consider fullwidth characters when rendering tabs', () => { @@ -1735,7 +1735,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22832: Consider fullwidth characters when rendering tabs (render whitespace)', () => { @@ -1774,7 +1774,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', () => { @@ -1807,7 +1807,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #22352: Partially Broken Complex Script Rendering of Tamil', () => { @@ -1842,7 +1842,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #42700: Hindi characters are not being rendered properly', () => { @@ -1877,7 +1877,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', () => { @@ -1909,7 +1909,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #33525: Long line with ligatures takes a long time to paint decorations', () => { @@ -1945,7 +1945,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #33525: Long line with ligatures takes a long time to paint decorations - not possible', () => { @@ -1977,7 +1977,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); test('issue #91936: Semantic token color highlighting fails on line with selected text', () => { @@ -2062,7 +2062,7 @@ suite('viewLineRenderer.renderLine 2', () => { '' ].join(''); - assert.deepEqual(actual.html, expected); + assert.deepStrictEqual(actual.html, expected); }); @@ -2092,7 +2092,7 @@ suite('viewLineRenderer.renderLine 2', () => { return (partIndex: number, partLength: number, offset: number, expected: number) => { let charOffset = renderLineOutput.characterMapping.partDataToCharOffset(partIndex, partLength, offset); let actual = charOffset + 1; - assert.equal(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); + assert.strictEqual(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); }; } diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 6e62f2e66a..c86169f068 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -47,6 +47,7 @@ function toAnnotatedText(text: string, lineBreakData: LineBreakData | null): str function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, text: string, previousLineBreakData: LineBreakData | null): LineBreakData | null { const fontInfo = new FontInfo({ zoomLevel: 0, + pixelRatio: 1, fontFamily: 'testFontFamily', fontWeight: 'normal', fontSize: 14, @@ -55,7 +56,7 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, letterSpacing: 0, isMonospace: true, typicalHalfwidthCharacterWidth: 7, - typicalFullwidthCharacterWidth: 14, + typicalFullwidthCharacterWidth: 7 * columnsForFullWidthChar, canUseHalfwidthRightwardsArrow: true, spaceWidth: 7, middotWidth: 7, @@ -74,7 +75,7 @@ function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null); const actualAnnotatedText = toAnnotatedText(text, lineBreakData); - assert.equal(actualAnnotatedText, annotatedText); + assert.strictEqual(actualAnnotatedText, annotatedText); return lineBreakData; } @@ -122,28 +123,41 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { assertLineBreaks(factory, 4, 5, 'aa.(.|).aaa'); }); - function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None): void { + function assertLineBreakDataEqual(a: LineBreakData | null, b: LineBreakData | null): void { + if (!a || !b) { + assert.deepStrictEqual(a, b); + return; + } + assert.deepStrictEqual(a.breakOffsets, b.breakOffsets); + assert.deepStrictEqual(a.wrappedTextIndentLength, b.wrappedTextIndentLength); + for (let i = 0; i < a.breakOffsetsVisibleColumn.length; i++) { + const diff = a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i]; + assert.ok(diff < 0.001); + } + } + + function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None, columnsForFullWidthChar: number = 2): void { // sanity check the test - assert.equal(text, parseAnnotatedText(annotatedText1).text); - assert.equal(text, parseAnnotatedText(annotatedText2).text); + assert.strictEqual(text, parseAnnotatedText(annotatedText1).text); + assert.strictEqual(text, parseAnnotatedText(annotatedText2).text); // check that the direct mapping is ok for 1 - const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, null); - assert.equal(toAnnotatedText(text, directLineBreakData1), annotatedText1); + const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, text, null); + assert.strictEqual(toAnnotatedText(text, directLineBreakData1), annotatedText1); // check that the direct mapping is ok for 2 - const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, null); - assert.equal(toAnnotatedText(text, directLineBreakData2), annotatedText2); + const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, text, null); + assert.strictEqual(toAnnotatedText(text, directLineBreakData2), annotatedText2); // check that going from 1 to 2 is ok - const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, directLineBreakData1); - assert.equal(toAnnotatedText(text, lineBreakData2from1), annotatedText2); - assert.deepEqual(lineBreakData2from1, directLineBreakData2); + const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, text, directLineBreakData1); + assert.strictEqual(toAnnotatedText(text, lineBreakData2from1), annotatedText2); + assertLineBreakDataEqual(lineBreakData2from1, directLineBreakData2); // check that going from 2 to 1 is ok - const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, directLineBreakData2); - assert.equal(toAnnotatedText(text, lineBreakData1from2), annotatedText1); - assert.deepEqual(lineBreakData1from2, directLineBreakData1); + const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, text, directLineBreakData2); + assert.strictEqual(toAnnotatedText(text, lineBreakData1from2), annotatedText1); + assertLineBreakDataEqual(lineBreakData1from2, directLineBreakData1); } test('MonospaceLineBreaksComputer incremental 1', () => { @@ -216,6 +230,19 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { ); }); + test('issue #110392: Occasional crash when resize with panel on the right', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertIncrementalLineBreaks( + factory, + '你好 **hello** **hello** **hello-world** hey there!', + 4, + 15, '你好 **hello** |**hello** |**hello-world**| hey there!', + 1, '你|好| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|*|*| |*|*|h|e|l|l|o|-|w|o|r|l|d|*|*| |h|e|y| |t|h|e|r|e|!', + WrappingIndent.Same, + 1.6605405405405405 + ); + }); + test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => { let factory = new MonospaceLineBreaksComputerFactory('(', '\t)'); assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89'); @@ -239,7 +266,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { test('issue #35162: wrappingIndent not consistently working', () => { let factory = new MonospaceLineBreaksComputerFactory('', '\t '); let mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent); - assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length); }); test('issue #75494: surrogate pairs', () => { @@ -260,11 +287,16 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => { let factory = new MonospaceLineBreaksComputerFactory('', '\t '); let mapper = assertLineBreaks(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent); - assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + assert.strictEqual(mapper!.wrappedTextIndentLength, ' '.length); }); test('issue #33366: Word wrap algorithm behaves differently around punctuation', () => { const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); assertLineBreaks(factory, 4, 23, 'this is a line of |text, text that sits |on a line', WrappingIndent.Same); }); + + test('issue #112382: Word wrap doesn\'t work well with control characters', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 6, '\x06\x06\x06|\x06\x06\x06', WrappingIndent.Same); + }); }); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 74a5e05d64..099b250922 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -22,158 +22,158 @@ suite('Editor ViewModel - PrefixSumComputer', () => { let indexOfResult: PrefixSumIndexOfResult; let psc = new PrefixSumComputer(toUint32Array([1, 1, 2, 1, 3])); - assert.equal(psc.getTotalValue(), 8); - assert.equal(psc.getAccumulatedValue(-1), 0); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 2); - assert.equal(psc.getAccumulatedValue(2), 4); - assert.equal(psc.getAccumulatedValue(3), 5); - assert.equal(psc.getAccumulatedValue(4), 8); + assert.strictEqual(psc.getTotalValue(), 8); + assert.strictEqual(psc.getAccumulatedValue(-1), 0); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 2); + assert.strictEqual(psc.getAccumulatedValue(2), 4); + assert.strictEqual(psc.getAccumulatedValue(3), 5); + assert.strictEqual(psc.getAccumulatedValue(4), 8); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 1); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 1); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(6); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(7); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(8); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 2, 2, 1, 3] psc.changeValue(1, 2); - assert.equal(psc.getTotalValue(), 9); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 3); - assert.equal(psc.getAccumulatedValue(2), 5); - assert.equal(psc.getAccumulatedValue(3), 6); - assert.equal(psc.getAccumulatedValue(4), 9); + assert.strictEqual(psc.getTotalValue(), 9); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 3); + assert.strictEqual(psc.getAccumulatedValue(2), 5); + assert.strictEqual(psc.getAccumulatedValue(3), 6); + assert.strictEqual(psc.getAccumulatedValue(4), 9); // [1, 0, 2, 1, 3] psc.changeValue(1, 0); - assert.equal(psc.getTotalValue(), 7); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 3); - assert.equal(psc.getAccumulatedValue(3), 4); - assert.equal(psc.getAccumulatedValue(4), 7); + assert.strictEqual(psc.getTotalValue(), 7); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 3); + assert.strictEqual(psc.getAccumulatedValue(3), 4); + assert.strictEqual(psc.getAccumulatedValue(4), 7); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 2); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 2); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(6); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(7); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 1, 3] psc.changeValue(2, 0); - assert.equal(psc.getTotalValue(), 5); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 1); - assert.equal(psc.getAccumulatedValue(3), 2); - assert.equal(psc.getAccumulatedValue(4), 5); + assert.strictEqual(psc.getTotalValue(), 5); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 1); + assert.strictEqual(psc.getAccumulatedValue(3), 2); + assert.strictEqual(psc.getAccumulatedValue(4), 5); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(5); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 0, 3] psc.changeValue(3, 0); - assert.equal(psc.getTotalValue(), 4); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 1); - assert.equal(psc.getAccumulatedValue(2), 1); - assert.equal(psc.getAccumulatedValue(3), 1); - assert.equal(psc.getAccumulatedValue(4), 4); + assert.strictEqual(psc.getTotalValue(), 4); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 1); + assert.strictEqual(psc.getAccumulatedValue(2), 1); + assert.strictEqual(psc.getAccumulatedValue(3), 1); + assert.strictEqual(psc.getAccumulatedValue(4), 4); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 2); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 2); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 3); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 3); // [1, 1, 0, 1, 1] psc.changeValue(1, 1); psc.changeValue(3, 1); psc.changeValue(4, 1); - assert.equal(psc.getTotalValue(), 4); - assert.equal(psc.getAccumulatedValue(0), 1); - assert.equal(psc.getAccumulatedValue(1), 2); - assert.equal(psc.getAccumulatedValue(2), 2); - assert.equal(psc.getAccumulatedValue(3), 3); - assert.equal(psc.getAccumulatedValue(4), 4); + assert.strictEqual(psc.getTotalValue(), 4); + assert.strictEqual(psc.getAccumulatedValue(0), 1); + assert.strictEqual(psc.getAccumulatedValue(1), 2); + assert.strictEqual(psc.getAccumulatedValue(2), 2); + assert.strictEqual(psc.getAccumulatedValue(3), 3); + assert.strictEqual(psc.getAccumulatedValue(4), 4); indexOfResult = psc.getIndexOf(0); - assert.equal(indexOfResult.index, 0); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 0); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(1); - assert.equal(indexOfResult.index, 1); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 1); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(2); - assert.equal(indexOfResult.index, 3); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 3); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(3); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 0); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 0); indexOfResult = psc.getIndexOf(4); - assert.equal(indexOfResult.index, 4); - assert.equal(indexOfResult.remainder, 1); + assert.strictEqual(indexOfResult.index, 4); + assert.strictEqual(indexOfResult.remainder, 1); }); }); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index dac1007a0d..df20fba823 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -25,42 +25,42 @@ suite('Editor ViewModel - SplitLinesCollection', () => { let model1 = createModel('My First LineMy Second LineAnd another one'); let line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 0); - assert.equal(line1.getViewLineCount(), 3); - assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); - assert.equal(line1.getViewLineContent(model1, 1, 1), 'My Second Line'); - assert.equal(line1.getViewLineContent(model1, 1, 2), 'And another one'); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 15); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 16); + assert.strictEqual(line1.getViewLineCount(), 3); + assert.strictEqual(line1.getViewLineContent(model1, 1, 0), 'My First Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 1), 'My Second Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 2), 'And another one'); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 0), 14); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 1), 15); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 2), 16); for (let col = 1; col <= 14; col++) { - assert.equal(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); } for (let col = 1; col <= 15; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, col), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(1, col), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); } for (let col = 1; col <= 16; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); + assert.strictEqual(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); } for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); } model1 = createModel('My First LineMy Second LineAnd another one'); line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 4); - assert.equal(line1.getViewLineCount(), 3); - assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); - assert.equal(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); - assert.equal(line1.getViewLineContent(model1, 1, 2), ' And another one'); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 19); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 20); + assert.strictEqual(line1.getViewLineCount(), 3); + assert.strictEqual(line1.getViewLineContent(model1, 1, 0), 'My First Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); + assert.strictEqual(line1.getViewLineContent(model1, 1, 2), ' And another one'); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 0), 14); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 1), 19); + assert.strictEqual(line1.getViewLineMaxColumn(model1, 1, 2), 20); let actualViewColumnMapping: number[][] = []; for (let lineIndex = 0; lineIndex < line1.getViewLineCount(); lineIndex++) { @@ -70,20 +70,20 @@ suite('Editor ViewModel - SplitLinesCollection', () => { } actualViewColumnMapping.push(actualLineViewColumnMapping); } - assert.deepEqual(actualViewColumnMapping, [ + assert.deepStrictEqual(actualViewColumnMapping, [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], [14, 14, 14, 14, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], [28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43], ]); for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); + assert.deepStrictEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); } }); @@ -136,65 +136,65 @@ suite('Editor ViewModel - SplitLinesCollection', () => { ].join('\n'); withSplitLinesCollection(text, (model, linesCollection) => { - assert.equal(linesCollection.getViewLineCount(), 6); + assert.strictEqual(linesCollection.getViewLineCount(), 6); // getOutputIndentGuide - assert.deepEqual(linesCollection.getViewLinesIndentGuides(-1, -1), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(0, 0), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(1, 1), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(2, 2), [1]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(3, 3), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(4, 4), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(5, 5), [1]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(6, 6), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(7, 7), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(-1, -1), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(0, 0), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(1, 1), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(2, 2), [1]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(3, 3), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(4, 4), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(5, 5), [1]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(6, 6), [0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(7, 7), [0]); - assert.deepEqual(linesCollection.getViewLinesIndentGuides(0, 7), [0, 1, 0, 0, 1, 0]); + assert.deepStrictEqual(linesCollection.getViewLinesIndentGuides(0, 7), [0, 1, 0, 0, 1, 0]); // getOutputLineContent - assert.equal(linesCollection.getViewLineContent(-1), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(0), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(1), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(2), '\tprintf("Hello world!");'); - assert.equal(linesCollection.getViewLineContent(3), '}'); - assert.equal(linesCollection.getViewLineContent(4), 'int main() {'); - assert.equal(linesCollection.getViewLineContent(5), '\tprintf("Hello world!");'); - assert.equal(linesCollection.getViewLineContent(6), '}'); - assert.equal(linesCollection.getViewLineContent(7), '}'); + assert.strictEqual(linesCollection.getViewLineContent(-1), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(0), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(1), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(2), '\tprintf("Hello world!");'); + assert.strictEqual(linesCollection.getViewLineContent(3), '}'); + assert.strictEqual(linesCollection.getViewLineContent(4), 'int main() {'); + assert.strictEqual(linesCollection.getViewLineContent(5), '\tprintf("Hello world!");'); + assert.strictEqual(linesCollection.getViewLineContent(6), '}'); + assert.strictEqual(linesCollection.getViewLineContent(7), '}'); // getOutputLineMinColumn - assert.equal(linesCollection.getViewLineMinColumn(-1), 1); - assert.equal(linesCollection.getViewLineMinColumn(0), 1); - assert.equal(linesCollection.getViewLineMinColumn(1), 1); - assert.equal(linesCollection.getViewLineMinColumn(2), 1); - assert.equal(linesCollection.getViewLineMinColumn(3), 1); - assert.equal(linesCollection.getViewLineMinColumn(4), 1); - assert.equal(linesCollection.getViewLineMinColumn(5), 1); - assert.equal(linesCollection.getViewLineMinColumn(6), 1); - assert.equal(linesCollection.getViewLineMinColumn(7), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(-1), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(0), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(1), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(2), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(3), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(4), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(5), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(6), 1); + assert.strictEqual(linesCollection.getViewLineMinColumn(7), 1); // getOutputLineMaxColumn - assert.equal(linesCollection.getViewLineMaxColumn(-1), 13); - assert.equal(linesCollection.getViewLineMaxColumn(0), 13); - assert.equal(linesCollection.getViewLineMaxColumn(1), 13); - assert.equal(linesCollection.getViewLineMaxColumn(2), 25); - assert.equal(linesCollection.getViewLineMaxColumn(3), 2); - assert.equal(linesCollection.getViewLineMaxColumn(4), 13); - assert.equal(linesCollection.getViewLineMaxColumn(5), 25); - assert.equal(linesCollection.getViewLineMaxColumn(6), 2); - assert.equal(linesCollection.getViewLineMaxColumn(7), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(-1), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(0), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(1), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(2), 25); + assert.strictEqual(linesCollection.getViewLineMaxColumn(3), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(4), 13); + assert.strictEqual(linesCollection.getViewLineMaxColumn(5), 25); + assert.strictEqual(linesCollection.getViewLineMaxColumn(6), 2); + assert.strictEqual(linesCollection.getViewLineMaxColumn(7), 2); // convertOutputPositionToInputPosition - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(-1, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(0, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(1, 1), new Position(1, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(2, 1), new Position(2, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(3, 1), new Position(3, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(4, 1), new Position(4, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(5, 1), new Position(5, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(6, 1), new Position(6, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(7, 1), new Position(6, 1)); - assert.deepEqual(linesCollection.convertViewPositionToModelPosition(8, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(-1, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(0, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(1, 1), new Position(1, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(2, 1), new Position(2, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(3, 1), new Position(3, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(4, 1), new Position(4, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(5, 1), new Position(5, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(6, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(7, 1), new Position(6, 1)); + assert.deepStrictEqual(linesCollection.convertViewPositionToModelPosition(8, 1), new Position(6, 1)); }); }); @@ -216,7 +216,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { ]); let viewLineCount = linesCollection.getViewLineCount(); - assert.equal(viewLineCount, 1, 'getOutputLineCount()'); + assert.strictEqual(viewLineCount, 1, 'getOutputLineCount()'); let modelLineCount = model.getLineCount(); for (let lineNumber = 0; lineNumber <= modelLineCount + 1; lineNumber++) { @@ -244,7 +244,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { viewColumn = viewMaxColumn; } let validViewPosition = new Position(viewLineNumber, viewColumn); - assert.equal(viewPosition.toString(), validViewPosition.toString(), 'model->view for ' + lineNumber + ', ' + column); + assert.strictEqual(viewPosition.toString(), validViewPosition.toString(), 'model->view for ' + lineNumber + ', ' + column); } } @@ -254,7 +254,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { for (let column = lineMinColumn - 1; column <= lineMaxColumn + 1; column++) { let modelPosition = linesCollection.convertViewPositionToModelPosition(lineNumber, column); let validModelPosition = model.validatePosition(modelPosition); - assert.equal(modelPosition.toString(), validModelPosition.toString(), 'view->model for ' + lineNumber + ', ' + column); + assert.strictEqual(modelPosition.toString(), validModelPosition.toString(), 'view->model for ' + lineNumber + ', ' + column); } } }); @@ -333,7 +333,7 @@ suite('SplitLinesCollection', () => { const tokenizationSupport: modes.ITokenizationSupport = { getInitialState: () => NULL_STATE, tokenize: undefined!, - tokenize2: (line: string, state: modes.IState): TokenizationResult2 => { + tokenize2: (line: string, hasEOL: boolean, state: modes.IState): TokenizationResult2 => { let tokens = _tokens[_lineIndex++]; let result = new Uint32Array(2 * tokens.length); @@ -374,7 +374,7 @@ suite('SplitLinesCollection', () => { value: _actual.getForeground(i) }; } - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); } interface ITestMinimapLineRenderingData { @@ -391,16 +391,15 @@ suite('SplitLinesCollection', () => { } if (expected === null) { assert.ok(false); - return; } - assert.equal(actual.content, expected.content); - assert.equal(actual.minColumn, expected.minColumn); - assert.equal(actual.maxColumn, expected.maxColumn); + assert.strictEqual(actual.content, expected.content); + assert.strictEqual(actual.minColumn, expected.minColumn); + assert.strictEqual(actual.maxColumn, expected.maxColumn); assertViewLineTokens(actual.tokens, expected.tokens); } function assertMinimapLinesRenderingData(actual: ViewLineData[], expected: Array): void { - assert.equal(actual.length, expected.length); + assert.strictEqual(actual.length, expected.length); for (let i = 0; i < expected.length; i++) { assertMinimapLineRenderingData(actual[i], expected[i]); } @@ -429,15 +428,15 @@ suite('SplitLinesCollection', () => { test('getViewLinesData - no wrapping', () => { withSplitLinesCollection(model!, 'off', 0, (splitLinesCollection) => { - assert.equal(splitLinesCollection.getViewLineCount(), 8); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); let _expected: ITestMinimapLineRenderingData[] = [ { @@ -541,15 +540,15 @@ suite('SplitLinesCollection', () => { ]); splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); - assert.equal(splitLinesCollection.getViewLineCount(), 5); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 5); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); assertAllMinimapLinesRenderingData(splitLinesCollection, [ _expected[0], @@ -563,15 +562,15 @@ suite('SplitLinesCollection', () => { test('getViewLinesData - with wrapping', () => { withSplitLinesCollection(model!, 'wordWrapColumn', 30, (splitLinesCollection) => { - assert.equal(splitLinesCollection.getViewLineCount(), 12); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 12); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); let _expected: ITestMinimapLineRenderingData[] = [ { @@ -711,15 +710,15 @@ suite('SplitLinesCollection', () => { ]); splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); - assert.equal(splitLinesCollection.getViewLineCount(), 8); - assert.equal(splitLinesCollection.modelPositionIsVisible(1, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(2, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(3, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(4, 1), false); - assert.equal(splitLinesCollection.modelPositionIsVisible(5, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(6, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(7, 1), true); - assert.equal(splitLinesCollection.modelPositionIsVisible(8, 1), true); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); assertAllMinimapLinesRenderingData(splitLinesCollection, [ _expected[0], diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index 8c9b4c59a3..af4043ea15 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; -import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; +import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; import { testViewModel } from 'vs/editor/test/common/viewModel/testViewModel'; suite('ViewModelDecorations', () => { @@ -19,11 +19,11 @@ suite('ViewModelDecorations', () => { wordWrapColumn: 13 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineContent(1), 'hello world, '); - assert.equal(viewModel.getLineContent(2), 'this is a '); - assert.equal(viewModel.getLineContent(3), 'buffer that '); - assert.equal(viewModel.getLineContent(4), 'will be '); - assert.equal(viewModel.getLineContent(5), 'wrapped'); + assert.strictEqual(viewModel.getLineContent(1), 'hello world, '); + assert.strictEqual(viewModel.getLineContent(2), 'this is a '); + assert.strictEqual(viewModel.getLineContent(3), 'buffer that '); + assert.strictEqual(viewModel.getLineContent(4), 'will be '); + assert.strictEqual(viewModel.getLineContent(5), 'wrapped'); model.changeDecorations((accessor) => { let createOpts = (id: string) => { @@ -79,7 +79,7 @@ suite('ViewModelDecorations', () => { return dec.options.className; }).filter(Boolean); - assert.deepEqual(actualDecorations, [ + assert.deepStrictEqual(actualDecorations, [ 'dec1', 'dec2', 'dec3', @@ -102,112 +102,28 @@ suite('ViewModelDecorations', () => { ).inlineDecorations; // view line 2: (1,14 -> 1,24) - assert.deepEqual(inlineDecorations1, [ - { - range: new Range(1, 2, 2, 2), - inlineClassName: 'i-dec3', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 2, 2, 2), - inlineClassName: 'a-dec3', - type: InlineDecorationType.After - }, - { - range: new Range(1, 2, 3, 13), - inlineClassName: 'i-dec4', - type: InlineDecorationType.Regular - }, - { - range: new Range(1, 2, 5, 8), - inlineClassName: 'i-dec5', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'i-dec6', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec6', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'a-dec6', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 2, 3), - inlineClassName: 'i-dec7', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec7', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'a-dec7', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 3, 13), - inlineClassName: 'i-dec8', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec8', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 1, 5, 8), - inlineClassName: 'i-dec9', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 2, 1), - inlineClassName: 'b-dec9', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 2, 5), - inlineClassName: 'i-dec10', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec10', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 5, 2, 5), - inlineClassName: 'a-dec10', - type: InlineDecorationType.After - }, - { - range: new Range(2, 3, 3, 13), - inlineClassName: 'i-dec11', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec11', - type: InlineDecorationType.Before - }, - { - range: new Range(2, 3, 5, 8), - inlineClassName: 'i-dec12', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 2, 3), - inlineClassName: 'b-dec12', - type: InlineDecorationType.Before - }, + assert.deepStrictEqual(inlineDecorations1, [ + new InlineDecoration(new Range(1, 2, 2, 2), 'i-dec3', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 2, 2, 2), 'a-dec3', InlineDecorationType.After), + new InlineDecoration(new Range(1, 2, 3, 13), 'i-dec4', InlineDecorationType.Regular), + new InlineDecoration(new Range(1, 2, 5, 8), 'i-dec5', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'i-dec6', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec6', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 1, 2, 1), 'a-dec6', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 2, 3), 'i-dec7', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec7', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 2, 3), 'a-dec7', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 3, 13), 'i-dec8', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec8', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 1, 5, 8), 'i-dec9', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 2, 1), 'b-dec9', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 2, 5), 'i-dec10', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec10', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 5, 2, 5), 'a-dec10', InlineDecorationType.After), + new InlineDecoration(new Range(2, 3, 3, 13), 'i-dec11', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec11', InlineDecorationType.Before), + new InlineDecoration(new Range(2, 3, 5, 8), 'i-dec12', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 2, 3), 'b-dec12', InlineDecorationType.Before), ]); let inlineDecorations2 = viewModel.getViewLineRenderingData( @@ -216,52 +132,16 @@ suite('ViewModelDecorations', () => { ).inlineDecorations; // view line 3 (24 -> 36) - assert.deepEqual(inlineDecorations2, [ - { - range: new Range(1, 2, 3, 13), - inlineClassName: 'i-dec4', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec4', - type: InlineDecorationType.After - }, - { - range: new Range(1, 2, 5, 8), - inlineClassName: 'i-dec5', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 1, 3, 13), - inlineClassName: 'i-dec8', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec8', - type: InlineDecorationType.After - }, - { - range: new Range(2, 1, 5, 8), - inlineClassName: 'i-dec9', - type: InlineDecorationType.Regular - }, - { - range: new Range(2, 3, 3, 13), - inlineClassName: 'i-dec11', - type: InlineDecorationType.Regular - }, - { - range: new Range(3, 13, 3, 13), - inlineClassName: 'a-dec11', - type: InlineDecorationType.After - }, - { - range: new Range(2, 3, 5, 8), - inlineClassName: 'i-dec12', - type: InlineDecorationType.Regular - }, + assert.deepStrictEqual(inlineDecorations2, [ + new InlineDecoration(new Range(1, 2, 3, 13), 'i-dec4', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec4', InlineDecorationType.After), + new InlineDecoration(new Range(1, 2, 5, 8), 'i-dec5', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 1, 3, 13), 'i-dec8', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec8', InlineDecorationType.After), + new InlineDecoration(new Range(2, 1, 5, 8), 'i-dec9', InlineDecorationType.Regular), + new InlineDecoration(new Range(2, 3, 3, 13), 'i-dec11', InlineDecorationType.Regular), + new InlineDecoration(new Range(3, 13, 3, 13), 'a-dec11', InlineDecorationType.After), + new InlineDecoration(new Range(2, 3, 5, 8), 'i-dec12', InlineDecorationType.Regular), ]); }); }); @@ -275,11 +155,11 @@ suite('ViewModelDecorations', () => { wordWrapColumn: 13 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineContent(1), 'hello world, '); - assert.equal(viewModel.getLineContent(2), 'this is a '); - assert.equal(viewModel.getLineContent(3), 'buffer that '); - assert.equal(viewModel.getLineContent(4), 'will be '); - assert.equal(viewModel.getLineContent(5), 'wrapped'); + assert.strictEqual(viewModel.getLineContent(1), 'hello world, '); + assert.strictEqual(viewModel.getLineContent(2), 'this is a '); + assert.strictEqual(viewModel.getLineContent(3), 'buffer that '); + assert.strictEqual(viewModel.getLineContent(4), 'will be '); + assert.strictEqual(viewModel.getLineContent(5), 'wrapped'); model.changeDecorations((accessor) => { accessor.addDecoration( @@ -293,19 +173,19 @@ suite('ViewModelDecorations', () => { let decorations = viewModel.getDecorationsInViewport( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) ).filter(x => Boolean(x.options.beforeContentClassName)); - assert.deepEqual(decorations, []); + assert.deepStrictEqual(decorations, []); let inlineDecorations1 = viewModel.getViewLineRenderingData( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)), 2 ).inlineDecorations; - assert.deepEqual(inlineDecorations1, []); + assert.deepStrictEqual(inlineDecorations1, []); let inlineDecorations2 = viewModel.getViewLineRenderingData( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)), 3 ).inlineDecorations; - assert.deepEqual(inlineDecorations2, []); + assert.deepStrictEqual(inlineDecorations2, []); }); }); @@ -329,17 +209,9 @@ suite('ViewModelDecorations', () => { new Range(1, 1, 1, 1), 1 ).inlineDecorations; - assert.deepEqual(inlineDecorations, [ - { - range: new Range(1, 1, 1, 1), - inlineClassName: 'before1', - type: InlineDecorationType.Before - }, - { - range: new Range(1, 1, 1, 1), - inlineClassName: 'after1', - type: InlineDecorationType.After - } + assert.deepStrictEqual(inlineDecorations, [ + new InlineDecoration(new Range(1, 1, 1, 1), 'before1', InlineDecorationType.Before), + new InlineDecoration(new Range(1, 1, 1, 1), 'after1', InlineDecorationType.After) ]); }); }); diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index e14395a056..91345f4b4a 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -18,7 +18,7 @@ suite('ViewModel', () => { lineNumbersMinChars: 1 }; testViewModel(text, opts, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); viewModel.setViewport(1, 1, 1); @@ -38,14 +38,14 @@ suite('ViewModel', () => { ].join('\n') }]); - assert.equal(viewModel.getLineCount(), 10); + assert.strictEqual(viewModel.getLineCount(), 10); }); }); test('issue #44805: SplitLinesCollection: attempt to access a \'newer\' model', () => { const text = ['']; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); model.pushEditOperations([], [{ range: new Range(1, 1, 1, 1), @@ -74,7 +74,7 @@ suite('ViewModel', () => { model.undo(); viewLineCount.push(viewModel.getLineCount()); - assert.deepEqual(viewLineCount, [4, 1, 1, 1, 1]); + assert.deepStrictEqual(viewLineCount, [4, 1, 1, 1, 1]); }); }); @@ -85,7 +85,7 @@ suite('ViewModel', () => { 'line3' ]; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 3); + assert.strictEqual(viewModel.getLineCount(), 3); viewModel.setHiddenAreas([new Range(1, 1, 3, 1)]); assert.ok(viewModel.getVisibleRanges() !== null); }); @@ -96,7 +96,7 @@ suite('ViewModel', () => { '' ]; testViewModel(text, {}, (viewModel, model) => { - assert.equal(viewModel.getLineCount(), 1); + assert.strictEqual(viewModel.getLineCount(), 1); model.pushEditOperations([], [{ range: new Range(1, 1, 1, 1), @@ -104,7 +104,7 @@ suite('ViewModel', () => { }], () => ([])); viewModel.setHiddenAreas([new Range(1, 1, 1, 1)]); - assert.equal(viewModel.getLineCount(), 2); + assert.strictEqual(viewModel.getLineCount(), 2); model.undo(); assert.ok(viewModel.getVisibleRanges() !== null); @@ -114,7 +114,7 @@ suite('ViewModel', () => { function assertGetPlainTextToCopy(text: string[], ranges: Range[], emptySelectionClipboard: boolean, expected: string | string[]): void { testViewModel(text, {}, (viewModel, model) => { let actual = viewModel.getPlainTextToCopy(ranges, emptySelectionClipboard, false); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); } @@ -259,7 +259,39 @@ suite('ViewModel', () => { testViewModel(USUAL_TEXT, {}, (viewModel, model) => { model.setEOL(EndOfLineSequence.LF); let actual = viewModel.getPlainTextToCopy([new Range(2, 1, 5, 1)], true, true); - assert.deepEqual(actual, 'line2\r\nline3\r\nline4\r\n'); + assert.deepStrictEqual(actual, 'line2\r\nline3\r\nline4\r\n'); }); }); + + test('issue #40926: Incorrect spacing when inserting new line after multiple folded blocks of code', () => { + testViewModel( + [ + 'foo = {', + ' foobar: function() {', + ' this.foobar();', + ' },', + ' foobar: function() {', + ' this.foobar();', + ' },', + ' foobar: function() {', + ' this.foobar();', + ' },', + '}', + ], {}, (viewModel, model) => { + viewModel.setHiddenAreas([ + new Range(3, 1, 3, 1), + new Range(6, 1, 6, 1), + new Range(9, 1, 9, 1), + ]); + + model.applyEdits([ + { range: new Range(4, 7, 4, 7), text: '\n ' }, + { range: new Range(7, 7, 7, 7), text: '\n ' }, + { range: new Range(10, 7, 10, 7), text: '\n ' } + ]); + + assert.strictEqual(viewModel.getLineCount(), 11); + } + ); + }); }); diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts index 3d140c275f..c1c2b7f98d 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -126,7 +126,7 @@ function executeTest(fileName: string, parseFunc: IParseFunc): void { actual[3 * actualIndex] + actual[3 * actualIndex + 1] >= assertion.startOffset + assertion.length, `Line ${assertion.testLineNumber} : length : ${actual[3 * actualIndex]} + ${actual[3 * actualIndex + 1]} >= ${assertion.startOffset} + ${assertion.length}.` ); - assert.equal( + assert.strictEqual( actual[3 * actualIndex + 2], assertion.tokenType, `Line ${assertion.testLineNumber} : tokenType`); diff --git a/src/vs/loader.js b/src/vs/loader.js index f5f647956b..709006c16b 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -36,7 +36,7 @@ var AMDLoader; this._detect(); return this._isWindows; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isNode", { @@ -44,7 +44,7 @@ var AMDLoader; this._detect(); return this._isNode; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isElectronRenderer", { @@ -52,7 +52,7 @@ var AMDLoader; this._detect(); return this._isElectronRenderer; }, - enumerable: true, + enumerable: false, configurable: true }); Object.defineProperty(Environment.prototype, "isWebWorker", { @@ -60,7 +60,7 @@ var AMDLoader; this._detect(); return this._isWebWorker; }, - enumerable: true, + enumerable: false, configurable: true }); Environment.prototype._detect = function () { @@ -199,6 +199,7 @@ var AMDLoader; return obj; } if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) { + // only clone "simple" objects return obj; } var result = Array.isArray(obj) ? [] : {}; @@ -741,8 +742,8 @@ var AMDLoader; // nothing } }; - require.resolve = function resolve(request) { - return Module._resolveFilename(request, mod); + require.resolve = function resolve(request, options) { + return Module._resolveFilename(request, mod, false, options); }; require.main = process.mainModule; require.extensions = Module._extensions; @@ -863,7 +864,7 @@ var AMDLoader; } }; NodeScriptLoader.prototype._getCachedDataPath = function (config, filename) { - var hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed, 'utf8').digest('hex'); + var hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed, 'utf8').update(process.arch, '').digest('hex'); var basename = this._path.basename(filename).replace(/\.js$/, ''); return this._path.join(config.path, basename + "-" + hash + ".code"); }; @@ -1218,6 +1219,7 @@ var AMDLoader; this._requireFunc = requireFunc; this._moduleIdProvider = new ModuleIdProvider(); this._config = new AMDLoader.Configuration(this._env); + this._hasDependencyCycle = false; this._modules2 = []; this._knownModules2 = []; this._inverseDependencies2 = []; @@ -1562,6 +1564,9 @@ var AMDLoader; result.getStats = function () { return _this.getLoaderEvents(); }; + result.hasDependencyCycle = function () { + return _this._hasDependencyCycle; + }; result.config = function (params, shouldOverwrite) { if (shouldOverwrite === void 0) { shouldOverwrite = false; } _this.configure(params, shouldOverwrite); @@ -1667,6 +1672,7 @@ var AMDLoader; continue; } if (this._hasDependencyPath(dependency.id, module.id)) { + this._hasDependencyCycle = true; console.warn('There is a dependency cycle between \'' + this._moduleIdProvider.getStrModuleId(dependency.id) + '\' and \'' + this._moduleIdProvider.getStrModuleId(module.id) + '\'. The cyclic path follows:'); var cyclePath = this._findCyclePath(dependency.id, module.id, 0) || []; cyclePath.reverse(); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 4666cfbb11..d88df279a5 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -10,6 +10,7 @@ declare namespace monaco { export type Thenable = PromiseLike; export interface Environment { + globalAPI?: boolean; baseUrl?: string; getWorker?(workerId: string, label: string): Worker; getWorkerUrl?(workerId: string, label: string): string; @@ -897,6 +898,12 @@ declare namespace monaco.editor { take?: number; }): IMarker[]; + /** + * Emitted when markers change for a model. + * @event + */ + export function onDidChangeMarkers(listener: (e: readonly Uri[]) => void): IDisposable; + /** * Get the model that has `uri` if it exists. */ @@ -969,6 +976,11 @@ declare namespace monaco.editor { */ export function remeasureFonts(): void; + /** + * Register a command. + */ + export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable; + export type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black'; export interface IStandaloneThemeData { @@ -1890,11 +1902,15 @@ declare namespace monaco.editor { */ detectIndentation(defaultInsertSpaces: boolean, defaultTabSize: number): void; /** - * Push a stack element onto the undo stack. This acts as an undo/redo point. - * The idea is to use `pushEditOperations` to edit the model and then to - * `pushStackElement` to create an undo/redo stop point. + * Close the current undo-redo element. + * This offers a way to create an undo/redo stop point. */ pushStackElement(): void; + /** + * Open the current undo-redo element. + * This offers a way to remove the current undo/redo stop point. + */ + popStackElement(): void; /** * Push edit operations, basically editing the model. This is the preferred way * of editing the model. The edit operations will land on the undo stack. @@ -2588,14 +2604,6 @@ declare namespace monaco.editor { Full = 4 } - export enum InDiffEditorState { - None = 0, - SideBySideLeft = 1, - SideBySideRight = 2, - InlineLeft = 3, - InlineRight = 4 - } - /** * Configuration options for the editor. */ @@ -2603,7 +2611,7 @@ declare namespace monaco.editor { /** * This editor is used inside a diff editor. */ - inDiffEditor?: InDiffEditorState; + inDiffEditor?: boolean; /** * The aria label for the editor's textarea (when it is focused). */ @@ -2819,6 +2827,14 @@ declare namespace monaco.editor { * Defaults to "off". */ wordWrap?: 'off' | 'on' | 'wordWrapColumn' | 'bounded'; + /** + * Override the `wordWrap` setting. + */ + wordWrapOverride1?: 'off' | 'on' | 'inherit'; + /** + * Override the `wordWrapOverride1` setting. + */ + wordWrapOverride2?: 'off' | 'on' | 'inherit'; /** * Control the wrapping of the editor. * When `wordWrap` = "off", the lines will never wrap. @@ -2828,11 +2844,6 @@ declare namespace monaco.editor { * Defaults to 80. */ wordWrapColumn?: number; - /** - * Force word wrapping when the text appears to be of a minified/generated file. - * Defaults to true. - */ - wordWrapMinified?: boolean; /** * Control indentation of wrapped lines. Can be: 'none', 'same', 'indent' or 'deepIndent'. * Defaults to 'same' in vscode and to 'none' in monaco-editor. @@ -3177,6 +3188,10 @@ declare namespace monaco.editor { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -3227,6 +3242,11 @@ declare namespace monaco.editor { * Defaults to false */ isInEmbeddedEditor?: boolean; + /** + * Is the diff editor should render overview ruler + * Defaults to true + */ + renderOverviewRuler?: boolean; /** * Control the wrapping of the diff editor. */ @@ -3527,6 +3547,29 @@ declare namespace monaco.editor { export type EditorLightbulbOptions = Readonly>; + /** + * Configuration options for editor inlineHints + */ + export interface IEditorInlineHintsOptions { + /** + * Enable the inline hints. + * Defaults to true. + */ + enabled?: boolean; + /** + * Font size of inline hints. + * Default to 90% of the editor font size. + */ + fontSize?: number; + /** + * Font family of inline hints. + * Defaults to editor font family. + */ + fontFamily?: string; + } + + export type EditorInlineHintsOptions = Readonly>; + /** * Configuration options for editor minimap */ @@ -4023,15 +4066,17 @@ declare namespace monaco.editor { wordWrapBreakAfterCharacters = 112, wordWrapBreakBeforeCharacters = 113, wordWrapColumn = 114, - wordWrapMinified = 115, - wrappingIndent = 116, - wrappingStrategy = 117, - showDeprecated = 118, - editorClassName = 119, - pixelRatio = 120, - tabFocusMode = 121, - layoutInfo = 122, - wrappingInfo = 123 + wordWrapOverride1 = 115, + wordWrapOverride2 = 116, + wrappingIndent = 117, + wrappingStrategy = 118, + showDeprecated = 119, + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4084,7 +4129,7 @@ declare namespace monaco.editor { hideCursorInOverviewRuler: IEditorOption; highlightActiveIndentGuide: IEditorOption; hover: IEditorOption; - inDiffEditor: IEditorOption; + inDiffEditor: IEditorOption; letterSpacing: IEditorOption; lightbulb: IEditorOption; lineDecorationsWidth: IEditorOption; @@ -4132,6 +4177,7 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; + inlineHints: IEditorOption; snippetSuggestions: IEditorOption; smartSelect: IEditorOption; smoothScrolling: IEditorOption; @@ -4150,7 +4196,8 @@ declare namespace monaco.editor { wordWrapBreakAfterCharacters: IEditorOption; wordWrapBreakBeforeCharacters: IEditorOption; wordWrapColumn: IEditorOption; - wordWrapMinified: IEditorOption; + wordWrapOverride1: IEditorOption; + wordWrapOverride2: IEditorOption; wrappingIndent: IEditorOption; wrappingStrategy: IEditorOption; editorClassName: IEditorOption; @@ -4495,6 +4542,10 @@ declare namespace monaco.editor { } export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: IDimension; /** * Place overflow widgets inside an external DOM node. * Defaults to an internal DOM node. @@ -4747,9 +4798,13 @@ declare namespace monaco.editor { */ executeCommand(source: string | null | undefined, command: ICommand): void; /** - * Push an "undo stop" in the undo-redo stack. + * Create an "undo stop" in the undo-redo stack. */ pushUndoStop(): boolean; + /** + * Remove the "undo stop" in the undo-redo stack. + */ + popUndoStop(): boolean; /** * Execute edits on the editor. * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. @@ -4934,6 +4989,7 @@ declare namespace monaco.editor { export class FontInfo extends BareFontInfo { readonly _editorStylingBrand: void; + readonly version: number; readonly isTrusted: boolean; readonly isMonospace: boolean; readonly typicalHalfwidthCharacterWidth: number; @@ -4948,6 +5004,7 @@ declare namespace monaco.editor { export class BareFontInfo { readonly _bareFontInfoBrand: void; readonly zoomLevel: number; + readonly pixelRatio: number; readonly fontFamily: string; readonly fontWeight: string; readonly fontSize: number; @@ -5068,8 +5125,18 @@ declare namespace monaco.languages { * Tokenize a line given the state at the beginning of the line. */ tokenizeEncoded(line: string, state: IState): IEncodedLineTokens; + /** + * Tokenize a line given the state at the beginning of the line. + */ + tokenize?(line: string, state: IState): ILineTokens; } + /** + * Change the color map that is used for token colors. + * Supported formats (hex): #RRGGBB, $RRGGBBAA, #RGB, #RGBA + */ + export function setColorMap(colorMap: string[] | null): void; + /** * Set the tokens provider for a language (manual implementation). */ @@ -5361,7 +5428,7 @@ declare namespace monaco.languages { /** * This rule will only execute if the text above the this line matches this regular expression. */ - oneLineAboveText?: RegExp; + previousLineText?: RegExp; /** * The action to execute. */ @@ -6347,6 +6414,19 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } + export interface InlineHint { + text: string; + range: IRange; + description?: string | IMarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + } + + export interface InlineHintsProvider { + onDidChangeInlineHints?: IEvent | undefined; + provideInlineHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; + } + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -6424,6 +6504,15 @@ declare namespace monaco.languages { * attach this to every token class (by default '.' + name) */ tokenPostfix?: string; + /** + * include line feeds (in the form of a \n character) at the end of lines + * Defaults to false + */ + includeLF?: boolean; + /** + * Other keys that can be referred to by the tokenizer. + */ + [key: string]: any; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css new file mode 100644 index 0000000000..6f52114b11 --- /dev/null +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-action-bar .action-item.menu-entry .action-label { + background-image: var(--menu-entry-icon-light); + display: inline-flex; +} + +.vs-dark .monaco-action-bar .action-item.menu-entry .action-label, +.hc-black .monaco-action-bar .action-item.menu-entry .action-label { + background-image: var(--menu-entry-icon-dark); +} diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index c08d56b99f..92eaac1cf8 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -3,10 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createCSSRule, asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom'; +import 'vs/css!./menuEntryActionViewItem'; +import { asCSSUrl, createCSSRule, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { domEvent } from 'vs/base/browser/event'; import { IAction, Separator } from 'vs/base/common/actions'; -import { IdGenerator } from 'vs/base/common/idGenerator'; +import { IdGenerator } from 'vs/base/common/idGenerator'; // {{SQL CARBON EDIT}} import { IDisposable, toDisposable, MutableDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; // {{SQL CARBON EDIT}} import { localize } from 'vs/nls'; import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions'; @@ -17,18 +18,20 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { isWindows, isLinux } from 'vs/base/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); - const useAlternativeActions = ModifierKeyEmitter.getInstance().keyStatus.altKey; + const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); + const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey); fillInActions(groups, target, useAlternativeActions, isPrimaryGroup); return asDisposable(groups); } -export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { +export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean, primaryMaxCount?: number): IDisposable { const groups = menu.getActions(options); // Action bars handle alternative actions on their own so the alternative actions should be ignored - fillInActions(groups, target, false, isPrimaryGroup); + fillInActions(groups, target, false, isPrimaryGroup, primaryMaxCount); return asDisposable(groups); } @@ -43,32 +46,43 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { - for (let tuple of groups) { - let [group, actions] = tuple; +export function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation', primaryMaxCount: number = Number.MAX_SAFE_INTEGER): void { + + let primaryBucket: IAction[]; + let secondaryBucket: IAction[]; + if (Array.isArray(target)) { + primaryBucket = target; + secondaryBucket = target; + } else { + primaryBucket = target.primary; + secondaryBucket = target.secondary; + } + + for (let [group, actions] of groups) { if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); } if (isPrimaryGroup(group)) { - const to = Array.isArray(target) ? target : target.primary; - - to.unshift(...actions); + primaryBucket.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; - - if (to.length > 0) { - to.push(new Separator()); + if (secondaryBucket.length > 0) { + secondaryBucket.push(new Separator()); } - - to.push(...actions); + secondaryBucket.push(...actions); } } + + // overflow items from the primary group into the secondary bucket + if (primaryBucket !== secondaryBucket && primaryBucket.length > primaryMaxCount) { + const overflow = primaryBucket.splice(primaryMaxCount, primaryBucket.length - primaryMaxCount); + secondaryBucket.unshift(...overflow, new Separator()); + } } -const ids = new IdGenerator('menu-item-action-item-icon-'); +const ids = new IdGenerator('menu-item-action-item-icon-'); // {{SQL CARBON EDIT}} - add back since custom toolbar menu is using below -const ICON_PATH_TO_CSS_RULES = new Map(); +const ICON_PATH_TO_CSS_RULES = new Map(); // {{SQL CARBON EDIT}} - add back since custom toolbar menu is using below export class MenuEntryActionViewItem extends ActionViewItem { @@ -85,7 +99,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { this._altKey = ModifierKeyEmitter.getInstance(); } - protected get _commandAction(): IAction { + protected get _commandAction(): MenuItemAction { return this._wantsAltCommand && (this._action).alt || this._action; } @@ -93,12 +107,14 @@ export class MenuEntryActionViewItem extends ActionViewItem { event.preventDefault(); event.stopPropagation(); - this.actionRunner.run(this._commandAction, this._context) - .then(undefined, err => this._notificationService.error(err)); + this.actionRunner + .run(this._commandAction, this._context) + .catch(err => this._notificationService.error(err)); } render(container: HTMLElement): void { super.render(container); + container.classList.add('menu-entry'); this._updateItemClass(this._action.item); @@ -167,46 +183,39 @@ export class MenuEntryActionViewItem extends ActionViewItem { protected _updateItemClass(item: ICommandAction): void { // {{SQL CARBON EDIT}} make it overwritable this._itemClassDispose.value = undefined; + const { element, label } = this; + if (!element || !label) { + return; + } + const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon; + if (!icon) { + return; + } + if (ThemeIcon.isThemeIcon(icon)) { // theme icons const iconClass = ThemeIcon.asClassName(icon); - if (this.label && iconClass) { - this.label.classList.add(...iconClass.split(' ')); - this._itemClassDispose.value = toDisposable(() => { - if (this.label) { - this.label.classList.remove(...iconClass.split(' ')); - } - }); + label.classList.add(...iconClass.split(' ')); + this._itemClassDispose.value = toDisposable(() => { + label.classList.remove(...iconClass.split(' ')); + }); + + } else { + // icon path/url + if (icon.light) { + label.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light)); } - - } else if (icon) { - // icon path - let iconClass: string; - - if (icon.dark?.scheme) { - - const iconPathMapKey = icon.dark.toString(); - - if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - iconClass = ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; - } else { - iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`); - ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); - } - - if (this.label) { - this.label.classList.add('icon', ...iconClass.split(' ')); - this._itemClassDispose.value = toDisposable(() => { - if (this.label) { - this.label.classList.remove('icon', ...iconClass.split(' ')); - } - }); - } + if (icon.dark) { + label.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark)); } + label.classList.add('icon'); + this._itemClassDispose.value = toDisposable(() => { + label.classList.remove('icon'); + label.style.removeProperty('--menu-entry-icon-light'); + label.style.removeProperty('--menu-entry-icon-dark'); + }); } } } @@ -215,30 +224,42 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { constructor( action: SubmenuItemAction, - @INotificationService _notificationService: INotificationService, - @IContextMenuService _contextMenuService: IContextMenuService + @IContextMenuService contextMenuService: IContextMenuService ) { - let classNames: string | string[] | undefined; + super(action, { getActions: () => action.actions }, contextMenuService, { + menuAsChild: true, + classNames: ThemeIcon.isThemeIcon(action.item.icon) ? ThemeIcon.asClassName(action.item.icon) : undefined, + }); + } - if (action.item.icon) { - if (ThemeIcon.isThemeIcon(action.item.icon)) { - classNames = ThemeIcon.asClassName(action.item.icon)!; - } else if (action.item.icon.dark?.scheme) { - const iconPathMapKey = action.item.icon.dark.toString(); - - if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - classNames = ['icon', ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!]; - } else { - const className = ids.nextId(); - classNames = ['icon', className]; - createCSSRule(`.icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.light || action.item.icon.dark)}`); - createCSSRule(`.vs-dark .icon.${className}, .hc-black .icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.dark)}`); - ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, className); + render(container: HTMLElement): void { + super.render(container); + if (this.element) { + container.classList.add('menu-entry'); + const { icon } = (this._action).item; + if (icon && !ThemeIcon.isThemeIcon(icon)) { + this.element.classList.add('icon'); + if (icon.light) { + this.element.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light)); + } + if (icon.dark) { + this.element.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark)); } } } + } +} - super(action, action.actions, _contextMenuService, { classNames: classNames, menuAsChild: true }); +/** + * Creates action view items for menu actions or submenu actions. + */ +export function createActionViewItem(instaService: IInstantiationService, action: IAction): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem { + if (action instanceof MenuItemAction) { + return instaService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return instaService.createInstance(SubmenuEntryActionViewItem, action); + } else { + return undefined; } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index c6d05de955..db9873b796 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -16,22 +16,36 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { UriDto } from 'vs/base/common/types'; import { Iterable } from 'vs/base/common/iterator'; import { LinkedList } from 'vs/base/common/linkedList'; +import { CSSIcon } from 'vs/base/common/codicons'; export interface ILocalizedString { + /** + * The localized value of the string. + */ value: string; + /** + * The original (non localized value of the string) + */ original: string; } +export interface ICommandActionTitle extends ILocalizedString { + /** + * The title with a mnemonic designation. && precedes the mnemonic. + */ + mnemonicTitle?: string; +} + export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; export interface ICommandAction { id: string; - title: string | ILocalizedString; + title: string | ICommandActionTitle; category?: string | ILocalizedString; - tooltip?: string | ILocalizedString; + tooltip?: string; icon?: Icon; precondition?: ContextKeyExpression; - toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString }; + toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string; }; } export type ISerializableCommandAction = UriDto; @@ -45,7 +59,7 @@ export interface IMenuItem { } export interface ISubmenuItem { - title: string | ILocalizedString; + title: string | ICommandActionTitle; submenu: MenuId; icon?: Icon; when?: ContextKeyExpression; @@ -112,6 +126,7 @@ export class MenuId { static readonly TunnelInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly ViewItemContext = new MenuId('ViewItemContext'); + static readonly ViewContainerTitle = new MenuId('ViewContainerTitle'); static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext'); static readonly ViewTitle = new MenuId('ViewTitle'); static readonly ViewTitleContext = new MenuId('ViewTitleContext'); @@ -141,6 +156,7 @@ export class MenuId { static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly AccountsContext = new MenuId('AccountsContext'); + static readonly PanelTitle = new MenuId('PanelTitle'); readonly id: number; readonly _debugName: string; @@ -157,7 +173,7 @@ export interface IMenuActionOptions { } export interface IMenu extends IDisposable { - readonly onDidChange: Event; + readonly onDidChange: Event; getActions(options?: IMenuActionOptions): [string, Array][]; } @@ -182,7 +198,7 @@ export interface IMenuRegistry { addCommand(userCommand: ICommandAction): IDisposable; getCommand(id: string): ICommandAction | undefined; getCommands(): ICommandsMap; - appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable; + appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem; }>): IDisposable; appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable; getMenuItems(loc: MenuId): Array; } @@ -233,7 +249,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry { return this.appendMenuItems(Iterable.single({ id, item })); } - appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable { + appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem; }>): IDisposable { const changedIds = new Set(); const toRemove = new LinkedList(); @@ -343,62 +359,76 @@ export class SubmenuItemAction extends SubmenuAction { } } -export class MenuItemAction extends ExecuteCommandAction { +// implements IAction, does NOT extend Action, so that no one +// subscribes to events of Action or modified properties +export class MenuItemAction implements IAction { readonly item: ICommandAction; readonly alt: MenuItemAction | undefined; + private readonly _options: IMenuActionOptions | undefined; - private _options: IMenuActionOptions; + readonly id: string; + + // {{SQL CARBON EDIT}} -- remove readonly since notebook component sets these + label: string; + tooltip: string; + readonly class: string | undefined; + readonly enabled: boolean; + readonly checked: boolean; + readonly expanded: boolean = false; constructor( item: ICommandAction, alt: ICommandAction | undefined, - options: IMenuActionOptions, + options: IMenuActionOptions | undefined, @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService + @ICommandService private _commandService: ICommandService ) { - typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService); - - this._cssClass = undefined; - this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); - this._tooltip = item.tooltip ? typeof item.tooltip === 'string' ? item.tooltip : item.tooltip.value : undefined; + this.id = item.id; + this.label = typeof item.title === 'string' ? item.title : item.title.value; + this.tooltip = item.tooltip ?? ''; + this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); + this.checked = false; if (item.toggled) { - const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as { - condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString + const toggled = ((item.toggled as { condition: ContextKeyExpression; }).condition ? item.toggled : { condition: item.toggled }) as { + condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString; }; - this._checked = contextKeyService.contextMatchesRules(toggled.condition); - if (this._checked && toggled.tooltip) { - this._tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; + this.checked = contextKeyService.contextMatchesRules(toggled.condition); + if (this.checked && toggled.tooltip) { + this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value; } } - this._options = options || {}; - this.item = item; - this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined; + this.alt = alt ? new MenuItemAction(alt, undefined, options, contextKeyService, _commandService) : undefined; + this._options = options; + if (ThemeIcon.isThemeIcon(item.icon)) { + this.class = CSSIcon.asClassName(item.icon); + } } dispose(): void { - if (this.alt) { - this.alt.dispose(); - } - super.dispose(); + // there is NOTHING to dispose and the MenuItemAction should + // never have anything to dispose as it is a convenience type + // to bridge into the rendering world. } run(...args: any[]): Promise { let runArgs: any[] = []; - if (this._options.arg) { + if (this._options?.arg) { runArgs = [...runArgs, this._options.arg]; } - if (this._options.shouldForwardArgs) { + if (this._options?.shouldForwardArgs) { runArgs = [...runArgs, ...args]; } - return super.run(...runArgs); + return this._commandService.executeCommand(this.id, ...runArgs); } + + } export class SyncActionDescriptor { @@ -411,7 +441,7 @@ export class SyncActionDescriptor { private readonly _keybindingContext: ContextKeyExpression | undefined; private readonly _keybindingWeight: number | undefined; - public static create(ctor: { new(id: string, label: string, ...services: Services): Action }, + public static create(ctor: { new(id: string, label: string, ...services: Services): Action; }, id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number ): SyncActionDescriptor { return new SyncActionDescriptor(ctor as IConstructorSignature2, id, label, keybindings, keybindingContext, keybindingWeight); @@ -478,7 +508,7 @@ export interface IAction2Options extends ICommandAction { /** * One or many menu items. */ - menu?: OneOrN<{ id: MenuId } & Omit>; + menu?: OneOrN<{ id: MenuId; } & Omit>; /** * One keybinding. @@ -497,7 +527,7 @@ export abstract class Action2 { abstract run(accessor: ServicesAccessor, ...args: any[]): any; } -export function registerAction2(ctor: { new(): Action2 }): IDisposable { +export function registerAction2(ctor: { new(): Action2; }): IDisposable { const disposables = new DisposableStore(); const action = new ctor(); diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 7c776296fb..2d48eca807 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -3,11 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService, IContextKeyChangeEvent, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class MenuService implements IMenuService { @@ -29,9 +30,11 @@ type MenuItemGroup = [string, Array]; class Menu implements IMenu { - private readonly _onDidChange = new Emitter(); private readonly _dispoables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + private _menuGroups: MenuItemGroup[] = []; private _contextKeys: Set = new Set(); @@ -45,19 +48,23 @@ class Menu implements IMenu { // rebuild this menu whenever the menu registry reports an // event for this MenuId - this._dispoables.add(Event.debounce( - Event.filter(MenuRegistry.onDidChangeMenu, set => set.has(this._id)), - () => { }, - 50 - )(this._build, this)); + const rebuildMenuSoon = new RunOnceScheduler(() => this._build(), 50); + this._dispoables.add(rebuildMenuSoon); + this._dispoables.add(MenuRegistry.onDidChangeMenu(e => { + if (e.has(_id)) { + rebuildMenuSoon.schedule(); + } + })); // when context keys change we need to check if the menu also // has changed - this._dispoables.add(Event.debounce( - this._contextKeyService.onDidChangeContext, - (last, event) => last || event.affectsSome(this._contextKeys), - 50 - )(e => e && this._onDidChange.fire(undefined), this)); + const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), 50); + this._dispoables.add(fireChangeSoon); + this._dispoables.add(_contextKeyService.onDidChangeContext(e => { + if (e.affectsSome(this._contextKeys)) { + fireChangeSoon.schedule(); + } + })); } dispose(): void { @@ -88,25 +95,22 @@ class Menu implements IMenu { // keep keys for eventing Menu._fillInKbExprKeys(item.when, this._contextKeys); - // keep precondition keys for event if applicable - if (isIMenuItem(item) && item.command.precondition) { - Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); - } - - // keep toggled keys for event if applicable - if (isIMenuItem(item) && item.command.toggled) { - const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled as ContextKeyExpression; // {{SQL CARBON EDIT}} strict-null-checks - Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + if (isIMenuItem(item)) { + // keep precondition keys for event if applicable + if (item.command.precondition) { + Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); + } + // keep toggled keys for event if applicable + if (item.command.toggled) { + const toggledExpression: any = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; + Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + } } } this._onDidChange.fire(this); } - get onDidChange(): Event { - return this._onDidChange.event; - } - - getActions(options: IMenuActionOptions): [string, Array][] { + getActions(options?: IMenuActionOptions): [string, Array][] { const result: [string, Array][] = []; for (let group of this._menuGroups) { const [id, items] = group; diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index ce9264eb7c..9c0837371a 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as crypto from 'crypto'; -import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; -import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs'; +import { createHash } from 'crypto'; +import { join } from 'vs/base/common/path'; +import { isLinux } from 'vs/base/common/platform'; +import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -35,7 +35,7 @@ export class BackupMainService implements IBackupMainService { // - ignore path casing on Windows/macOS // - respect path casing on Linux private readonly backupUriComparer = extUriBiasedIgnorePathCase; - private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !platform.isLinux) }; + private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !isLinux) }; constructor( @IEnvironmentMainService environmentService: IEnvironmentMainService, @@ -49,7 +49,7 @@ export class BackupMainService implements IBackupMainService { async initialize(): Promise { let backups: IBackupWorkspacesFormat; try { - backups = JSON.parse(await readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here + backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here } catch (error) { backups = Object.create(null); } @@ -151,8 +151,8 @@ export class BackupMainService implements IBackupMainService { if (fs.existsSync(moveFromPath)) { try { fs.renameSync(moveFromPath, backupPath); - } catch (ex) { - this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not move backup folder to new location: ${error.toString()}`); } } } @@ -204,7 +204,7 @@ export class BackupMainService implements IBackupMainService { } private getBackupPath(oldFolderHash: string): string { - return path.join(this.backupHome, oldFolderHash); + return join(this.backupHome, oldFolderHash); } private async validateWorkspaces(rootWorkspaces: IWorkspaceBackupInfo[]): Promise { @@ -312,8 +312,8 @@ export class BackupMainService implements IBackupMainService { if (await exists(backupPath)) { await rimraf(backupPath, RimRafMode.MOVE); } - } catch (ex) { - this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`); } } @@ -328,9 +328,9 @@ export class BackupMainService implements IBackupMainService { // Rename backupPath to new empty window backup path const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { - await rename(backupPath, newEmptyWindowBackupPath); - } catch (ex) { - this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); + await fs.promises.rename(backupPath, newEmptyWindowBackupPath); + } catch (error) { + this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; } this.emptyWindows.push({ backupFolder: newBackupFolder }); @@ -350,8 +350,8 @@ export class BackupMainService implements IBackupMainService { const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { fs.renameSync(backupPath, newEmptyWindowBackupPath); - } catch (ex) { - this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; } this.emptyWindows.push({ backupFolder: newBackupFolder }); @@ -406,7 +406,7 @@ export class BackupMainService implements IBackupMainService { for (const backupSchema of backupSchemas) { try { - const backupSchemaChildren = await readdir(path.join(backupPath, backupSchema)); + const backupSchemaChildren = await readdir(join(backupPath, backupSchema)); if (backupSchemaChildren.length > 0) { return true; } @@ -424,16 +424,16 @@ export class BackupMainService implements IBackupMainService { private saveSync(): void { try { writeFileSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); - } catch (ex) { - this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`); } } private async save(): Promise { try { await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups())); - } catch (ex) { - this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`); + } catch (error) { + this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`); } } @@ -454,11 +454,11 @@ export class BackupMainService implements IBackupMainService { if (folderUri.scheme === Schemas.file) { // for backward compatibility, use the fspath as key - key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); + key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); } else { key = folderUri.toString().toLowerCase(); } - return crypto.createHash('md5').update(key).digest('hex'); + return createHash('md5').update(key).digest('hex'); } } diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 5401421e3e..0633edd2bc 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -20,39 +20,14 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ConsoleLogMainService } from 'vs/platform/log/common/log'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createHash } from 'crypto'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -suite('BackupMainService', () => { +flakySuite('BackupMainService', () => { function assertEqualUris(actual: URI[], expected: URI[]) { - assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); - } - - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice'); - const backupHome = path.join(parentDir, 'Backups'); - const backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); - - const environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); - - class TestBackupMainService extends BackupMainService { - - constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) { - super(environmentService, configService, new ConsoleLogMainService()); - - this.backupHome = backupHome; - this.workspacesJsonPath = backupWorkspacesPath; - } - - toBackupPath(arg: URI | string): string { - const id = arg instanceof URI ? super.getFolderHash(arg) : arg; - return path.join(this.backupHome, id); - } - - getFolderHash(folderUri: URI): string { - return super.getFolderHash(folderUri); - } + assert.deepStrictEqual(actual.map(a => a.toString()), expected.map(a => a.toString())); } function toWorkspace(path: string): IWorkspaceIdentifier { @@ -79,20 +54,23 @@ suite('BackupMainService', () => { }; } - async function ensureFolderExists(uri: URI): Promise { + function ensureFolderExists(uri: URI): Promise { if (!fs.existsSync(uri.fsPath)) { fs.mkdirSync(uri.fsPath); } + const backupFolder = service.toBackupPath(uri); - await createBackupFolder(backupFolder); + return createBackupFolder(backupFolder); } async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise { if (!fs.existsSync(workspace.configPath.fsPath)) { await pfs.writeFile(workspace.configPath.fsPath, 'Hello'); } + const backupFolder = service.toBackupPath(workspace.id); await createBackupFolder(backupFolder); + return workspace; } @@ -111,29 +89,52 @@ suite('BackupMainService', () => { const fooFile = URI.file(platform.isWindows ? 'C:\\foo' : '/foo'); const barFile = URI.file(platform.isWindows ? 'C:\\bar' : '/bar'); - const existingTestFolder1 = URI.file(path.join(parentDir, 'folder1')); - - let service: TestBackupMainService; + let service: BackupMainService & { toBackupPath(arg: URI | string): string, getFolderHash(folderUri: URI): string }; let configService: TestConfigurationService; - setup(async () => { + let environmentService: EnvironmentMainService; + let testDir: string; + let backupHome: string; + let backupWorkspacesPath: string; + let existingTestFolder1: URI; - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); - await pfs.mkdirp(backupHome); + setup(async () => { + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupmainservice'); + backupHome = path.join(testDir, 'Backups'); + backupWorkspacesPath = path.join(backupHome, 'workspaces.json'); + existingTestFolder1 = URI.file(path.join(testDir, 'folder1')); + + environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS)); + + await fs.promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); - service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService); + service = new class TestBackupMainService extends BackupMainService { + constructor() { + super(environmentService, configService, new ConsoleLogMainService()); + + this.backupHome = backupHome; + this.workspacesJsonPath = backupWorkspacesPath; + } + + toBackupPath(arg: URI | string): string { + const id = arg instanceof URI ? super.getFolderHash(arg) : arg; + return path.join(this.backupHome, id); + } + + getFolderHash(folderUri: URI): string { + return super.getFolderHash(folderUri); + } + }; return service.initialize(); }); teardown(() => { - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + return pfs.rimraf(testDir); }); test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () { - this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist service.registerFolderBackupSync(fooFile); @@ -170,22 +171,21 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerFolderBackupSync(fooFile); - assert.equal(service.getFolderBackupPaths().length, 1); - assert.equal(service.getEmptyWindowBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); await service.initialize(); - assert.equal(service.getFolderBackupPaths().length, 0); - assert.equal(service.getEmptyWindowBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); test('service validates backup workspaces on startup and cleans up (root workspaces)', async function () { - this.timeout(1000 * 10); // increase timeout for this test // 1) backup workspace path does not exist service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); // 2) backup workspace path exists with empty contents within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -193,7 +193,7 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); @@ -205,7 +205,7 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); @@ -216,12 +216,12 @@ suite('BackupMainService', () => { fs.mkdirSync(service.toBackupPath(barFile)); fs.mkdirSync(fileBackups); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); - assert.equal(service.getWorkspaceBackups().length, 1); - assert.equal(service.getEmptyWindowBackupPaths().length, 0); + assert.strictEqual(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0); fs.writeFileSync(path.join(fileBackups, 'backup.txt'), ''); await service.initialize(); - assert.equal(service.getWorkspaceBackups().length, 0); - assert.equal(service.getEmptyWindowBackupPaths().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 0); + assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); test('service supports to migrate backup data from another location', () => { @@ -237,7 +237,7 @@ suite('BackupMainService', () => { assert.ok(!fs.existsSync(backupPathToMigrate)); const emptyBackups = service.getEmptyWindowBackupPaths(); - assert.equal(0, emptyBackups.length); + assert.strictEqual(0, emptyBackups.length); }); test('service backup migration makes sure to preserve existing backups', () => { @@ -258,8 +258,8 @@ suite('BackupMainService', () => { assert.ok(!fs.existsSync(backupPathToMigrate)); const emptyBackups = service.getEmptyWindowBackupPaths(); - assert.equal(1, emptyBackups.length); - assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length); + assert.strictEqual(1, emptyBackups.length); + assert.strictEqual(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length); }); suite('loadSync', () => { @@ -315,121 +315,120 @@ suite('BackupMainService', () => { }); test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when folderWorkspaces in workspaces.json is absent', async () => { fs.writeFileSync(backupWorkspacesPath, '{}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getWorkspaceBackups() should return [] when files.hotExit = "onExitAndWindowClose"', async () => { const upperFooPath = fooFile.fsPath.toUpperCase(); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); - assert.equal(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 1); assertEqualUris(service.getWorkspaceBackups().map(r => r.workspace.configPath), [URI.file(upperFooPath)]); configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE); await service.initialize(); - assert.deepEqual(service.getWorkspaceBackups(), []); + assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => { - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => { fs.writeFileSync(backupWorkspacesPath, '{}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async function () { - this.timeout(5000); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}'); await service.initialize(); - assert.deepEqual(service.getEmptyWindowBackupPaths(), []); + assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); }); @@ -446,9 +445,9 @@ suite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); test('should ignore duplicates on Windows and Mac (folder workspace)', async () => { @@ -462,16 +461,15 @@ suite('BackupMainService', () => { }; await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); test('should ignore duplicates on Windows and Mac (root workspace)', async () => { - - const workspacePath = path.join(parentDir, 'Foo.code-workspace'); - const workspacePath1 = path.join(parentDir, 'FOO.code-workspace'); - const workspacePath2 = path.join(parentDir, 'foo.code-workspace'); + const workspacePath = path.join(testDir, 'Foo.code-workspace'); + const workspacePath1 = path.join(testDir, 'FOO.code-workspace'); + const workspacePath2 = path.join(testDir, 'foo.code-workspace'); const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath)); const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath1)); @@ -485,13 +483,13 @@ suite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.equal(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); + assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]); } else { - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry'); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry'); } }); }); @@ -501,9 +499,9 @@ suite('BackupMainService', () => { service.registerFolderBackupSync(fooFile); service.registerFolderBackupSync(barFile); assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); test('should persist paths to workspaces.json (root workspace)', async () => { @@ -513,15 +511,15 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [fooFile, barFile]); - assert.equal(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); - assert.equal(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); + assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); + assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); - assert.equal(ws1.workspace.id, json.rootURIWorkspaces[0].id); - assert.equal(ws2.workspace.id, json.rootURIWorkspaces[1].id); + assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); + assert.strictEqual(ws1.workspace.id, json.rootURIWorkspaces[0].id); + assert.strictEqual(ws2.workspace.id, json.rootURIWorkspaces[1].id); }); }); @@ -529,9 +527,9 @@ suite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => { @@ -539,9 +537,9 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); suite('removeBackupPathSync', () => { @@ -550,14 +548,14 @@ suite('BackupMainService', () => { service.registerFolderBackupSync(barFile); service.unregisterFolderBackupSync(fooFile); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.folderURIWorkspaces, []); + assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); test('should remove folder workspaces from workspaces.json (root workspace)', async () => { @@ -567,14 +565,14 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); service.unregisterWorkspaceBackupSync(ws1.workspace); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); + assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.rootURIWorkspaces, []); + assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); test('should remove empty workspaces from workspaces.json', async () => { @@ -582,14 +580,14 @@ suite('BackupMainService', () => { service.registerEmptyWindowBackupSync('bar'); service.unregisterEmptyWindowBackupSync('foo'); - const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); - assert.deepEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); + assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); - assert.deepEqual(json2.emptyWorkspaceInfos, []); + assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); test('should fail gracefully when removing a path that doesn\'t exist', async () => { @@ -601,26 +599,20 @@ suite('BackupMainService', () => { await service.initialize(); service.unregisterFolderBackupSync(barFile); service.unregisterEmptyWindowBackupSync('test'); - const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); - assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); + assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); }); suite('getWorkspaceHash', () => { - - test('should ignore case on Windows and Mac', () => { - // Skip test on Linux - if (platform.isLinux) { - return; - } - + (platform.isLinux ? test.skip : test)('should ignore case on Windows and Mac', () => { if (platform.isMacintosh) { - assert.equal(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO'))); + assert.strictEqual(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO'))); } if (platform.isWindows) { - assert.equal(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO'))); + assert.strictEqual(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO'))); } }); }); @@ -631,9 +623,9 @@ suite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getFolderBackupPaths().length, 2); + assert.strictEqual(service.getFolderBackupPaths().length, 2); } else { - assert.equal(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 1); } }); @@ -642,9 +634,9 @@ suite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getWorkspaceBackups().length, 2); + assert.strictEqual(service.getWorkspaceBackups().length, 2); } else { - assert.equal(service.getWorkspaceBackups().length, 1); + assert.strictEqual(service.getWorkspaceBackups().length, 1); } }); @@ -653,16 +645,16 @@ suite('BackupMainService', () => { // same case service.registerFolderBackupSync(fooFile); service.unregisterFolderBackupSync(fooFile); - assert.equal(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 0); // mixed case service.registerFolderBackupSync(fooFile); service.unregisterFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); if (platform.isLinux) { - assert.equal(service.getFolderBackupPaths().length, 1); + assert.strictEqual(service.getFolderBackupPaths().length, 1); } else { - assert.equal(service.getFolderBackupPaths().length, 0); + assert.strictEqual(service.getFolderBackupPaths().length, 0); } }); }); @@ -674,22 +666,22 @@ suite('BackupMainService', () => { const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath); const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo); - assert.equal(((await service.getDirtyWorkspaces()).length), 0); + assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await pfs.mkdirp(path.join(folderBackupPath, Schemas.file)); - await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled)); + await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } - assert.equal(((await service.getDirtyWorkspaces()).length), 0); + assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), ''); fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), ''); const dirtyWorkspaces = await service.getDirtyWorkspaces(); - assert.equal(dirtyWorkspaces.length, 2); + assert.strictEqual(dirtyWorkspaces.length, 2); let found = 0; for (const dirtyWorkpspace of dirtyWorkspaces) { @@ -704,7 +696,7 @@ suite('BackupMainService', () => { } } - assert.equal(found, 2); + assert.strictEqual(found, 2); }); }); }); diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 1eb7684da9..c6aaf0539a 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { IFileService } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { IExtUri } from 'vs/base/common/resources'; export class ConfigurationModel implements IConfigurationModel { @@ -348,11 +348,12 @@ export class UserSettings extends Disposable { constructor( private readonly userSettingsResource: URI, private readonly scopes: ConfigurationScope[] | undefined, + extUri: IExtUri, private readonly fileService: IFileService ) { super(); this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); - this._register(this.fileService.watch(dirname(this.userSettingsResource))); + this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index ec800a4e05..d8f2271777 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -112,6 +112,13 @@ export interface IConfigurationPropertySchema extends IJSONSchema { scope?: ConfigurationScope; included?: boolean; tags?: string[]; + /** + * When enabled this setting is ignored during sync and user can override this. + */ + ignoreSync?: boolean; + /** + * When enabled this setting is ignored during sync and user cannot override this. + */ disallowSyncIgnore?: boolean; enumItemLabels?: string[]; } diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts index 37109270d5..d5c84de56c 100644 --- a/src/vs/platform/configuration/common/configurationService.ts +++ b/src/vs/platform/configuration/common/configurationService.ts @@ -12,6 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { @@ -29,7 +30,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe fileService: IFileService ) { super(); - this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService)); + this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService)); this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index 2dba7cfe1f..4ba034590a 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; +import { Emitter, PauseableEmitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { distinct } from 'vs/base/common/objects'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -244,12 +244,14 @@ class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent { } export abstract class AbstractContextKeyService implements IContextKeyService { - public _serviceBrand: undefined; + declare _serviceBrand: undefined; protected _isDisposed: boolean; - protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); protected _myContextId: number; + protected _onDidChangeContext = new PauseableEmitter({ merge: input => new CompositeContextKeyChangeEvent(input) }); + readonly onDidChangeContext = this._onDidChangeContext.event; + constructor(myContextId: number) { this._isDisposed = false; this._myContextId = myContextId; @@ -268,9 +270,6 @@ export abstract class AbstractContextKeyService implements IContextKeyService { return new ContextKey(this, key, defaultValue); } - public get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } bufferChangeEvents(callback: Function): void { this._onDidChangeContext.pause(); @@ -371,6 +370,7 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon } public dispose(): void { + this._onDidChangeContext.dispose(); this._isDisposed = true; this._toDispose.dispose(); } @@ -407,34 +407,34 @@ class ScopedContextKeyService extends AbstractContextKeyService { private _parent: AbstractContextKeyService; private _domNode: IContextKeyServiceTarget | undefined; - private _parentChangeListener: IDisposable | undefined; + private readonly _parentChangeListener = new MutableDisposable(); constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) { super(parent.createChildContext()); this._parent = parent; - this.updateParentChangeListener(); + this._updateParentChangeListener(); if (domNode) { this._domNode = domNode; if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) { - console.error('Element already has context attribute'); + let extraInfo = ''; + if ((this._domNode as HTMLElement).classList) { + extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', '); + } + + console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`); } this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId)); } } - private updateParentChangeListener(): void { - if (this._parentChangeListener) { - this._parentChangeListener.dispose(); - } - - this._parentChangeListener = this._parent.onDidChangeContext(e => { - // Forward parent events to this listener. Parent will change. - this._onDidChangeContext.fire(e); - }); + private _updateParentChangeListener(): void { + // Forward parent events to this listener. Parent will change. + this._parentChangeListener.value = this._parent.onDidChangeContext(this._onDidChangeContext.fire, this._onDidChangeContext); } public dispose(): void { + this._onDidChangeContext.dispose(); this._isDisposed = true; this._parent.disposeContext(this._myContextId); this._parentChangeListener?.dispose(); @@ -444,10 +444,6 @@ class ScopedContextKeyService extends AbstractContextKeyService { } } - public get onDidChangeContext(): Event { - return this._onDidChangeContext.event; - } - public getContextValuesContainer(contextId: number): Context { if (this._isDisposed) { return NullContext.INSTANCE; @@ -473,7 +469,7 @@ class ScopedContextKeyService extends AbstractContextKeyService { const thisContainer = this._parent.getContextValuesContainer(this._myContextId); const oldAllValues = thisContainer.collectAllValues(); this._parent = parentContextKeyService; - this.updateParentChangeListener(); + this._updateParentChangeListener(); const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId); thisContainer.updateParent(newParentContainer); diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index b1d24b50de..00979d4353 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -6,8 +6,9 @@ import { Event } from 'vs/base/common/event'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; +import { userAgent, isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; +let _userAgent = userAgent || ''; const STATIC_VALUES = new Map(); STATIC_VALUES.set('false', false); STATIC_VALUES.set('true', true); @@ -16,6 +17,11 @@ STATIC_VALUES.set('isLinux', isLinux); STATIC_VALUES.set('isWindows', isWindows); STATIC_VALUES.set('isWeb', isWeb); STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); +STATIC_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0); +STATIC_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0); +STATIC_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0); +STATIC_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0); +STATIC_VALUES.set('isIPad', _userAgent.indexOf('iPad') >= 0); const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -32,8 +38,10 @@ export const enum ContextKeyExprType { Or = 9, In = 10, NotIn = 11, - GreaterThanEquals = 12, // {{SQL CARBON EDIT}} add value - LessThanEquals = 13 // {{SQL CARBON EDIT}} add value + Greater = 12, + GreaterEquals = 13, + Smaller = 14, + SmallerEquals = 15, } export interface IContextKeyExprMapper { @@ -41,6 +49,10 @@ export interface IContextKeyExprMapper { mapNot(key: string): ContextKeyExpression; mapEquals(key: string, value: any): ContextKeyExpression; mapNotEquals(key: string, value: any): ContextKeyExpression; + mapGreater(key: string, value: any): ContextKeyExpression; + mapGreaterEquals(key: string, value: any): ContextKeyExpression; + mapSmaller(key: string, value: any): ContextKeyExpression; + mapSmallerEquals(key: string, value: any): ContextKeyExpression; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; mapIn(key: string, valueKey: string): ContextKeyInExpr; } @@ -59,7 +71,9 @@ export interface IContextKeyExpression { export type ContextKeyExpression = ( ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr - | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr | ContextKeyNotInExpr | ContextKeyGreaterThanEqualsExpr | ContextKeyLessThanEqualsExpr // {{SQL CARBON EDIT}} + | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr + | ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr + | ContextKeySmallerExpr | ContextKeySmallerEqualsExpr ); export abstract class ContextKeyExpr { @@ -104,15 +118,13 @@ export abstract class ContextKeyExpr { return ContextKeyOrExpr.create(expr); } - // {{SQL CARBON EDIT}} - public static greaterThanEquals(key: string, value: any): ContextKeyExpression { - return ContextKeyGreaterThanEqualsExpr.create(key, value); + public static greater(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterExpr.create(key, value); } - public static lessThanEquals(key: string, value: any): ContextKeyExpression { - return ContextKeyLessThanEqualsExpr.create(key, value); + public static less(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerExpr.create(key, value); } - // public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined { if (!serialized) { @@ -150,21 +162,31 @@ export abstract class ContextKeyExpr { return ContextKeyRegexExpr.create(pieces[0].trim(), this._deserializeRegexValue(pieces[1], strict)); } - // {{SQL CARBON EDIT}} - if (serializedOne.indexOf('>=') >= 0) { - let pieces = serializedOne.split('>='); - return ContextKeyGreaterThanEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); - } - if (serializedOne.indexOf('<=') >= 0) { - let pieces = serializedOne.split('<='); - return ContextKeyLessThanEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); - } - // if (serializedOne.indexOf(' in ') >= 0) { let pieces = serializedOne.split(' in '); return ContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim()); } + if (/^[^<=>]+>=[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('>='); + return ContextKeyGreaterEqualsExpr.create(pieces[0].trim(), pieces[1].trim()); + } + + if (/^[^<=>]+>[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('>'); + return ContextKeyGreaterExpr.create(pieces[0].trim(), pieces[1].trim()); + } + + if (/^[^<=>]+<=[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('<='); + return ContextKeySmallerEqualsExpr.create(pieces[0].trim(), pieces[1].trim()); + } + + if (/^[^<=>]+<[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('<'); + return ContextKeySmallerExpr.create(pieces[0].trim(), pieces[1].trim()); + } + if (/^\!\s*/.test(serializedOne)) { return ContextKeyNotExpr.create(serializedOne.substr(1).trim()); } @@ -324,13 +346,7 @@ export class ContextKeyDefinedExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - return 0; + return cmp1(this.key, other.key); } public equals(other: ContextKeyExpression): boolean { @@ -384,19 +400,7 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; + return cmp2(this.key, this.value, other.key, other.value); } public equals(other: ContextKeyExpression): boolean { @@ -444,19 +448,7 @@ export class ContextKeyInExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.valueKey < other.valueKey) { - return -1; - } - if (this.valueKey > other.valueKey) { - return 1; - } - return 0; + return cmp2(this.key, this.valueKey, other.key, other.valueKey); } public equals(other: ContextKeyExpression): boolean { @@ -571,19 +563,7 @@ export class ContextKeyNotEqualsExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; + return cmp2(this.key, this.value, other.key, other.value); } public equals(other: ContextKeyExpression): boolean { @@ -635,13 +615,7 @@ export class ContextKeyNotExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - return 0; + return cmp1(this.key, other.key); } public equals(other: ContextKeyExpression): boolean { @@ -672,6 +646,200 @@ export class ContextKeyNotExpr implements IContextKeyExpression { } } +export class ContextKeyGreaterExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeyGreaterExpr(key, value); + } + + public readonly type = ContextKeyExprType.Greater; + + private constructor( + private readonly key: string, + private readonly value: any + ) { } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) > parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} > ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapGreater(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(this.key, this.value); + } +} + +export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeyGreaterEqualsExpr(key, value); + } + + public readonly type = ContextKeyExprType.GreaterEquals; + + private constructor( + private readonly key: string, + private readonly value: any + ) { } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) >= parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} >= ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapGreaterEquals(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeySmallerExpr.create(this.key, this.value); + } +} + +export class ContextKeySmallerExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeySmallerExpr(key, value); + } + + public readonly type = ContextKeyExprType.Smaller; + + private constructor( + private readonly key: string, + private readonly value: any + ) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) < parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} < ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapSmaller(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(this.key, this.value); + } +} + +export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeySmallerEqualsExpr(key, value); + } + + public readonly type = ContextKeyExprType.SmallerEquals; + + private constructor( + private readonly key: string, + private readonly value: any + ) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) <= parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} <= ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapSmallerEquals(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeyGreaterExpr.create(this.key, this.value); + } +} + export class ContextKeyRegexExpr implements IContextKeyExpression { public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr { @@ -1089,152 +1257,6 @@ export class ContextKeyOrExpr implements IContextKeyExpression { } } -// {{SQL CARBON EDIT}} -export class ContextKeyGreaterThanEqualsExpr implements IContextKeyExpression { - - public static create(key: string, value: any): ContextKeyExpression { - if (typeof value === 'boolean') { - return (value ? ContextKeyDefinedExpr.create(key) : ContextKeyNotExpr.create(key)); - } - const staticValue = STATIC_VALUES.get(key); - if (typeof staticValue === 'boolean') { - const trueValue = staticValue ? 'true' : 'false'; - return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); - } - return new ContextKeyGreaterThanEqualsExpr(key, value); - } - - public readonly type = ContextKeyExprType.GreaterThanEquals; - - constructor(private key: string, private value: any) { - } - - public cmp(other: ContextKeyExpression): number { - if (other.type !== this.type) { - return this.type - other.type; - } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpression): boolean { - if (other.type === this.type) { - return (this.key === other.key && this.value === other.value); - } - return false; - } - - public negate(): ContextKeyExpression { - throw new Error('Method not implemented.'); // @TODO anthonydresser need to figure out what to do in this case - } - - public evaluate(context: IContext): boolean { - const keyVal = context.getValue(this.key); - if (!keyVal) { - return false; - } - const keyInt = parseFloat(keyVal); - const valueInt = parseFloat(this.value); - return (keyInt >= valueInt); - } - - public serialize(): string { - return this.key + ' >= \'' + this.value + '\''; - } - - public keys(): string[] { - return [this.key]; - } - - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { - return mapFnc.mapEquals(this.key, this.value); - } -} - -// {{SQL CARBON EDIT}} -export class ContextKeyLessThanEqualsExpr implements IContextKeyExpression { - - public static create(key: string, value: any): ContextKeyExpression { - if (typeof value === 'boolean') { - return (value ? ContextKeyDefinedExpr.create(key) : ContextKeyNotExpr.create(key)); - } - const staticValue = STATIC_VALUES.get(key); - if (typeof staticValue === 'boolean') { - const trueValue = staticValue ? 'true' : 'false'; - return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); - } - return new ContextKeyLessThanEqualsExpr(key, value); - } - - constructor(private key: string, private value: any) { - } - - public readonly type = ContextKeyExprType.LessThanEquals; - - public cmp(other: ContextKeyExpression): number { - if (other.type !== this.type) { - return this.type - other.type; - } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; - } - - public equals(other: ContextKeyExpression): boolean { - if (other.type === this.type) { - return (this.key === other.key && this.value === other.value); - } - return false; - } - - public evaluate(context: IContext): boolean { - const keyVal = context.getValue(this.key); - if (!keyVal) { - return false; - } - const keyInt = parseFloat(keyVal); - const valueInt = parseFloat(this.value); - return (keyInt <= valueInt); - } - - public negate(): ContextKeyExpression { - throw new Error('Method not implemented.'); // @TODO anthonydresser need to figure out what to do in this case - } - - public serialize(): string { - return this.key + ' <= \'' + this.value + '\''; - } - - public keys(): string[] { - return [this.key]; - } - - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { - return mapFnc.mapEquals(this.key, this.value); - } -} - export class RawContextKey extends ContextKeyDefinedExpr { private readonly _defaultValue: T | undefined; @@ -1256,11 +1278,11 @@ export class RawContextKey extends ContextKeyDefinedExpr { return ContextKeyExpr.not(this.key); } - public isEqualTo(value: string): ContextKeyExpression { + public isEqualTo(value: any): ContextKeyExpression { return ContextKeyExpr.equals(this.key, value); } - public notEqualsTo(value: string): ContextKeyExpression { + public notEqualsTo(value: any): ContextKeyExpression { return ContextKeyExpr.notEquals(this.key, value); } } @@ -1311,3 +1333,29 @@ export interface IContextKeyService { } export const SET_CONTEXT_COMMAND_ID = 'setContext'; + +function cmp1(key1: string, key2: string): number { + if (key1 < key2) { + return -1; + } + if (key1 > key2) { + return 1; + } + return 0; +} + +function cmp2(key1: string, value1: any, key2: string, value2: any): number { + if (key1 < key2) { + return -1; + } + if (key1 > key2) { + return 1; + } + if (value1 < value2) { + return -1; + } + if (value1 > value2) { + return 1; + } + return 0; +} diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 44945c378b..3fb2369cc4 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -28,16 +28,8 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.notEquals('c2', 'cc2'), ContextKeyExpr.not('d1'), ContextKeyExpr.not('d2'), - ContextKeyExpr.greaterThanEquals('e1', 'ee1'), // {{SQL CARBON EDIT}} add test case - ContextKeyExpr.greaterThanEquals('e2', 'ee2'), // {{SQL CARBON EDIT}} add test case - ContextKeyExpr.lessThanEquals('f1', 'ff1'), // {{SQL CARBON EDIT}} add test case - ContextKeyExpr.lessThanEquals('f2', 'ff2'), // {{SQL CARBON EDIT}} add test case )!; let b = ContextKeyExpr.and( - ContextKeyExpr.lessThanEquals('f1', 'ff1'), // {{SQL CARBON EDIT}} - ContextKeyExpr.lessThanEquals('f2', 'ff2'), // {{SQL CARBON EDIT}} - ContextKeyExpr.greaterThanEquals('e2', 'ee2'), // {{SQL CARBON EDIT}} - ContextKeyExpr.greaterThanEquals('e1', 'ee1'), // {{SQL CARBON EDIT}} ContextKeyExpr.equals('b2', 'bb2'), ContextKeyExpr.notEquals('c1', 'cc1'), ContextKeyExpr.not('d1'), @@ -75,7 +67,7 @@ suite('ContextKeyExpr', () => { function testExpression(expr: string, expected: boolean): void { // console.log(expr + ' ' + expected); let rules = ContextKeyExpr.deserialize(expr); - assert.equal(rules!.evaluate(context), expected, expr); + assert.strictEqual(rules!.evaluate(context), expected, expr); } function testBatch(expr: string, value: any): void { /* eslint-disable eqeqeq */ @@ -165,17 +157,17 @@ suite('ContextKeyExpr', () => { test('ContextKeyInExpr', () => { const ainb = ContextKeyExpr.deserialize('a in b')!; - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); - assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3 })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); + assert.strictEqual(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); }); test('issue #106524: distributing AND should normalize', () => { @@ -196,6 +188,86 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.has('c') ) ); - assert.equal(actual!.equals(expected!), true); + assert.strictEqual(actual!.equals(expected!), true); + }); + + test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => { + function checkEvaluate(expr: string, ctx: any, expected: any): void { + const _expr = ContextKeyExpr.deserialize(expr)!; + assert.strictEqual(_expr.evaluate(createContext(ctx)), expected); + } + + checkEvaluate('a>1', {}, false); + checkEvaluate('a>1', { a: 0 }, false); + checkEvaluate('a>1', { a: 1 }, false); + checkEvaluate('a>1', { a: 2 }, true); + checkEvaluate('a>1', { a: '0' }, false); + checkEvaluate('a>1', { a: '1' }, false); + checkEvaluate('a>1', { a: '2' }, true); + checkEvaluate('a>1', { a: 'a' }, false); + + checkEvaluate('a>10', { a: 2 }, false); + checkEvaluate('a>10', { a: 11 }, true); + checkEvaluate('a>10', { a: '11' }, true); + checkEvaluate('a>10', { a: '2' }, false); + checkEvaluate('a>10', { a: '11' }, true); + + checkEvaluate('a>1.1', { a: 1 }, false); + checkEvaluate('a>1.1', { a: 2 }, true); + checkEvaluate('a>1.1', { a: 11 }, true); + checkEvaluate('a>1.1', { a: '1.1' }, false); + checkEvaluate('a>1.1', { a: '2' }, true); + checkEvaluate('a>1.1', { a: '11' }, true); + + checkEvaluate('a>b', { a: 'b' }, false); + checkEvaluate('a>b', { a: 'c' }, false); + checkEvaluate('a>b', { a: 1000 }, false); + + checkEvaluate('a >= 2', { a: '1' }, false); + checkEvaluate('a >= 2', { a: '2' }, true); + checkEvaluate('a >= 2', { a: '3' }, true); + + checkEvaluate('a < 2', { a: '1' }, true); + checkEvaluate('a < 2', { a: '2' }, false); + checkEvaluate('a < 2', { a: '3' }, false); + + checkEvaluate('a <= 2', { a: '1' }, true); + checkEvaluate('a <= 2', { a: '2' }, true); + checkEvaluate('a <= 2', { a: '3' }, false); + }); + + test('Greater, GreaterEquals, Smaller, SmallerEquals negate', () => { + function checkNegate(expr: string, expected: string): void { + const a = ContextKeyExpr.deserialize(expr)!; + const b = a.negate(); + assert.strictEqual(b.serialize(), expected); + } + + checkNegate('a>1', 'a <= 1'); + checkNegate('a>1.1', 'a <= 1.1'); + checkNegate('a>b', 'a <= b'); + + checkNegate('a>=1', 'a < 1'); + checkNegate('a>=1.1', 'a < 1.1'); + checkNegate('a>=b', 'a < b'); + + checkNegate('a<1', 'a >= 1'); + checkNegate('a<1.1', 'a >= 1.1'); + checkNegate('a= b'); + + checkNegate('a<=1', 'a > 1'); + checkNegate('a<=1.1', 'a > 1.1'); + checkNegate('a<=b', 'a > b'); + }); + + test('issue #111899: context keys can use `<` or `>` ', () => { + const actual = ContextKeyExpr.deserialize('editorTextFocus && vim.active && vim.use')!; + assert.ok(actual.equals( + ContextKeyExpr.and( + ContextKeyExpr.has('editorTextFocus'), + ContextKeyExpr.has('vim.active'), + ContextKeyExpr.has('vim.use'), + )! + )); }); }); diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index b489d44ba0..c811c4abc5 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -158,7 +158,7 @@ export class ContextMenuHandler { } private onDidActionRun(e: IRunEvent): void { - if (e.error && this.notificationService) { + if (e.error) { this.notificationService.error(e.error); } } diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index 147b9e9534..6fbcc9d633 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -5,7 +5,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { IProcessEnvironment } from 'vs/base/common/platform'; export const IExtensionHostDebugService = createDecorator('extensionHostDebugService'); @@ -16,11 +15,6 @@ export interface IAttachSessionEvent { port: number; } -export interface ILogToSessionEvent { - sessionId: string; - log: IRemoteConsoleLog; -} - export interface ITerminateSessionEvent { sessionId: string; subId?: string; @@ -50,9 +44,6 @@ export interface IExtensionHostDebugService { attachSession(sessionId: string, port: number, subId?: string): void; readonly onAttachSession: Event; - logToSession(sessionId: string, log: IRemoteConsoleLog): void; - readonly onLogToSession: Event; - terminateSession(sessionId: string, subId?: string): void; readonly onTerminateSession: Event; diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index f8c267bc7e..d09ee2d192 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { Event, Emitter } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { Disposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -17,7 +16,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan private readonly _onCloseEmitter = new Emitter(); private readonly _onReloadEmitter = new Emitter(); private readonly _onTerminateEmitter = new Emitter(); - private readonly _onLogToEmitter = new Emitter(); private readonly _onAttachEmitter = new Emitter(); call(ctx: TContext, command: string, arg?: any): Promise { @@ -28,8 +26,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return Promise.resolve(this._onReloadEmitter.fire({ sessionId: arg[0] })); case 'terminate': return Promise.resolve(this._onTerminateEmitter.fire({ sessionId: arg[0] })); - case 'log': - return Promise.resolve(this._onLogToEmitter.fire({ sessionId: arg[0], log: arg[1] })); case 'attach': return Promise.resolve(this._onAttachEmitter.fire({ sessionId: arg[0], port: arg[1], subId: arg[2] })); } @@ -44,8 +40,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return this._onReloadEmitter.event; case 'terminate': return this._onTerminateEmitter.event; - case 'log': - return this._onLogToEmitter.event; case 'attach': return this._onAttachEmitter.event; } @@ -85,14 +79,6 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte return this.channel.listen('attach'); } - logToSession(sessionId: string, log: IRemoteConsoleLog): void { - this.channel.call('log', [sessionId, log]); - } - - get onLogToSession(): Event { - return this.channel.listen('log'); - } - terminateSession(sessionId: string, subId?: string): void { this.channel.call('terminate', [sessionId, subId]); } diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index 825cf36324..bc09ed2615 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -8,8 +8,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { createServer, AddressInfo } from 'net'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 8d6b8b3585..d5795028be 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -5,7 +5,7 @@ import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; -import { readdir, exists, readFile } from 'fs'; +import { exists, readFile } from 'fs'; import { join, basename } from 'vs/base/common/path'; import { parse, ParseError, getNodeType } from 'vs/base/common/json'; import { listProcesses } from 'vs/base/node/ps'; @@ -19,6 +19,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Iterable } from 'vs/base/common/iterator'; import { Schemas } from 'vs/base/common/network'; import { ByteSize } from 'vs/platform/files/common/files'; +import { IDirent, readdir } from 'vs/base/node/pfs'; export const ID = 'diagnosticsService'; export const IDiagnosticsService = createDecorator(ID); @@ -79,68 +80,69 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise { const relativePath = dir.substring(root.length + 1); - return new Promise(resolve => { - readdir(dir, { withFileTypes: true }, async (err, files) => { + return new Promise(async resolve => { + let files: IDirent[]; + try { + files = await readdir(dir, { withFileTypes: true }); + } catch (error) { // Ignore folders that can't be read - if (err) { - resolve(); - return; - } - - if (token.count >= MAX_FILES) { - token.count += files.length; - token.maxReached = true; - resolve(); - return; - } - - let pending = files.length; - if (pending === 0) { - resolve(); - return; - } - - let filesToRead = files; - if (token.count + files.length > MAX_FILES) { - token.maxReached = true; - pending = MAX_FILES - token.count; - filesToRead = files.slice(0, pending); - } + resolve(); + return; + } + if (token.count >= MAX_FILES) { token.count += files.length; + token.maxReached = true; + resolve(); + return; + } - for (const file of filesToRead) { - if (file.isDirectory()) { - if (!filter.includes(file.name)) { - await collect(root, join(dir, file.name), filter, token); - } + let pending = files.length; + if (pending === 0) { + resolve(); + return; + } - if (--pending === 0) { - resolve(); - return; - } - } else { - const index = file.name.lastIndexOf('.'); - if (index >= 0) { - const fileType = file.name.substring(index + 1); - if (fileType) { - fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1); - } - } + let filesToRead = files; + if (token.count + files.length > MAX_FILES) { + token.maxReached = true; + pending = MAX_FILES - token.count; + filesToRead = files.slice(0, pending); + } - for (const configFile of configFilePatterns) { - if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) { - configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1); - } - } + token.count += files.length; - if (--pending === 0) { - resolve(); - return; + for (const file of filesToRead) { + if (file.isDirectory()) { + if (!filter.includes(file.name)) { + await collect(root, join(dir, file.name), filter, token); + } + + if (--pending === 0) { + resolve(); + return; + } + } else { + const index = file.name.lastIndexOf('.'); + if (index >= 0) { + const fileType = file.name.substring(index + 1); + if (fileType) { + fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1); } } + + for (const configFile of configFilePatterns) { + if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) { + configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1); + } + } + + if (--pending === 0) { + resolve(); + return; + } } - }); + } }); } @@ -314,10 +316,10 @@ export class DiagnosticsService implements IDiagnosticsService { if (isLinux) { systemInfo.linuxEnv = { - desktopSession: process.env.DESKTOP_SESSION, - xdgSessionDesktop: process.env.XDG_SESSION_DESKTOP, - xdgCurrentDesktop: process.env.XDG_CURRENT_DESKTOP, - xdgSessionType: process.env.XDG_SESSION_TYPE + desktopSession: process.env['DESKTOP_SESSION'], + xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'], + xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'], + xdgSessionType: process.env['XDG_SESSION_TYPE'] }; } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 38b3490464..8fe965a8bd 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -181,6 +181,7 @@ export interface IDialogOptions { cancelId?: number; detail?: string; checkbox?: ICheckbox; + useCustom?: boolean; } export interface IInput { diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts similarity index 54% rename from src/vs/platform/dialogs/electron-main/dialogs.ts rename to src/vs/platform/dialogs/electron-main/dialogMainService.ts index 4cd8216f46..3c7c7fa70e 100644 --- a/src/vs/platform/dialogs/electron-main/dialogs.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -16,6 +16,8 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { hash } from 'vs/base/common/hash'; export const IDialogMainService = createDecorator('dialogMainService'); @@ -48,14 +50,13 @@ export class DialogMainService implements IDialogMainService { private static readonly workingDirPickerStorageKey = 'pickerWorkingDir'; - private readonly mapWindowToDialogQueue: Map>; - private readonly noWindowDialogQueue: Queue; + private readonly windowFileDialogLocks = new Map>(); + private readonly windowDialogQueues = new Map>(); + private readonly noWindowDialogueQueue = new Queue(); constructor( @IStateService private readonly stateService: IStateService ) { - this.mapWindowToDialogQueue = new Map>(); - this.noWindowDialogQueue = new Queue(); } pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { @@ -120,25 +121,28 @@ export class DialogMainService implements IDialogMainService { return result.filePaths; } - return undefined; // {{SQL CARBON EDIT}} strict-null-check + return undefined; } - private getDialogQueue(window?: BrowserWindow): Queue { - if (!window) { - return this.noWindowDialogQueue; - } + private getWindowDialogQueue(window?: BrowserWindow): Queue { - let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id); - if (!windowDialogQueue) { - windowDialogQueue = new Queue(); - this.mapWindowToDialogQueue.set(window.id, windowDialogQueue); - } + // Queue message box requests per window so that one can show + // after the other. + if (window) { + let windowDialogQueue = this.windowDialogQueues.get(window.id); + if (!windowDialogQueue) { + windowDialogQueue = new Queue(); + this.windowDialogQueues.set(window.id, windowDialogQueue); + } - return windowDialogQueue; + return windowDialogQueue as unknown as Queue; + } else { + return this.noWindowDialogueQueue as unknown as Queue; + } } showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise { - return this.getDialogQueue(window).queue(async () => { + return this.getWindowDialogQueue(window).queue(async () => { if (window) { return dialog.showMessageBox(window, options); } @@ -147,61 +151,115 @@ export class DialogMainService implements IDialogMainService { }); } - showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { + async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { - function normalizePath(path: string | undefined): string | undefined { - if (path && isMacintosh) { - path = normalizeNFC(path); // normalize paths returned from the OS - } - - return path; + // prevent duplicates of the same dialog queueing at the same time + const fileDialogLock = this.acquireFileDialogLock(options, window); + if (!fileDialogLock) { + throw new Error('A file save dialog is already or will be showing for the window with the same configuration'); } - return this.getDialogQueue(window).queue(async () => { - let result: SaveDialogReturnValue; - if (window) { - result = await dialog.showSaveDialog(window, options); - } else { - result = await dialog.showSaveDialog(options); - } + try { + return await this.getWindowDialogQueue(window).queue(async () => { + let result: SaveDialogReturnValue; + if (window) { + result = await dialog.showSaveDialog(window, options); + } else { + result = await dialog.showSaveDialog(options); + } - result.filePath = normalizePath(result.filePath); + result.filePath = this.normalizePath(result.filePath); - return result; - }); + return result; + }); + } finally { + dispose(fileDialogLock); + } } - showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { - - function normalizePaths(paths: string[]): string[] { - if (paths && paths.length > 0 && isMacintosh) { - paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS - } - - return paths; + private normalizePath(path: string): string; + private normalizePath(path: string | undefined): string | undefined; + private normalizePath(path: string | undefined): string | undefined { + if (path && isMacintosh) { + path = normalizeNFC(path); // macOS only: normalize paths to NFC form } - return this.getDialogQueue(window).queue(async () => { + return path; + } - // Ensure the path exists (if provided) - if (options.defaultPath) { - const pathExists = await exists(options.defaultPath); - if (!pathExists) { - options.defaultPath = undefined; + private normalizePaths(paths: string[]): string[] { + return paths.map(path => this.normalizePath(path)); + } + + async showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { + + // Ensure the path exists (if provided) + if (options.defaultPath) { + const pathExists = await exists(options.defaultPath); + if (!pathExists) { + options.defaultPath = undefined; + } + } + + // prevent duplicates of the same dialog queueing at the same time + const fileDialogLock = this.acquireFileDialogLock(options, window); + if (!fileDialogLock) { + throw new Error('A file open dialog is already or will be showing for the window with the same configuration'); + } + + try { + return await this.getWindowDialogQueue(window).queue(async () => { + let result: OpenDialogReturnValue; + if (window) { + result = await dialog.showOpenDialog(window, options); + } else { + result = await dialog.showOpenDialog(options); } + + result.filePaths = this.normalizePaths(result.filePaths); + + return result; + }); + } finally { + dispose(fileDialogLock); + } + } + + private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined { + + // if no window is provided, allow as many dialogs as + // needed since we consider them not modal per window + if (!window) { + return Disposable.None; + } + + // if a window is provided, only allow a single dialog + // at the same time because dialogs are modal and we + // do not want to open one dialog after the other + // (https://github.com/microsoft/vscode/issues/114432) + // we figure this out by `hashing` the configuration + // options for the dialog to prevent duplicates + + let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id); + if (!windowFileDialogLocks) { + windowFileDialogLocks = new Set(); + this.windowFileDialogLocks.set(window.id, windowFileDialogLocks); + } + + const optionsHash = hash(options); + if (windowFileDialogLocks.has(optionsHash)) { + return undefined; // prevent duplicates, return + } + + windowFileDialogLocks.add(optionsHash); + + return toDisposable(() => { + windowFileDialogLocks?.delete(optionsHash); + + // if the window has no more dialog locks, delete it from the set of locks + if (windowFileDialogLocks?.size === 0) { + this.windowFileDialogLocks.delete(window.id); } - - // Show dialog - let result: OpenDialogReturnValue; - if (window) { - result = await dialog.showOpenDialog(window, options); - } else { - result = await dialog.showOpenDialog(options); - } - - result.filePaths = normalizePaths(result.filePaths); - - return result; }); } } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 0f92476bbe..3871bb9b86 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -28,6 +28,7 @@ export interface NativeParsedArgs { 'prof-startup'?: boolean; 'prof-startup-prefix'?: string; 'prof-append-timers'?: string; + 'prof-v8-extensions'?: boolean; verbose?: boolean; trace?: boolean; 'trace-category-filter'?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 04aee73012..e699392fe9 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -53,8 +53,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, - 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, - 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, + 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") }, + 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") }, 'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") }, 'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, 'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, @@ -66,6 +66,7 @@ export const OPTIONS: OptionDescriptions> = { 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") }, 'prof-append-timers': { type: 'string' }, 'prof-startup-prefix': { type: 'string' }, + 'prof-v8-extensions': { type: 'boolean' }, 'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") }, 'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") }, 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, @@ -158,10 +159,6 @@ export function parseArgs(args: string[], options: OptionDescriptions, err const string: string[] = []; const boolean: string[] = []; for (let optionId in options) { - if (optionId[0] === '_') { - continue; - } - const o = options[optionId]; if (o.alias) { alias[optionId] = o.alias; diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index 40ee93229d..dadb0596d3 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -13,35 +13,35 @@ suite('EnvironmentService', () => { test('parseExtensionHostPort when built', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true); - assert.deepEqual(parse([]), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); - assert.deepEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); }); test('parseExtensionHostPort when unbuilt', () => { const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false); - assert.deepEqual(parse([]), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse([]), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); - assert.deepEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); - assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); - assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); + assert.deepStrictEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined }); + assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' }); }); test('userDataPath', () => { @@ -57,10 +57,10 @@ suite('EnvironmentService', () => { test('careful with boolean file names', function () { let actual = parseArgs(['-r', 'arg.txt'], OPTIONS); assert(actual['reuse-window']); - assert.deepEqual(actual._, ['arg.txt']); + assert.deepStrictEqual(actual._, ['arg.txt']); actual = parseArgs(['-r', 'true.txt'], OPTIONS); assert(actual['reuse-window']); - assert.deepEqual(actual._, ['true.txt']); + assert.deepStrictEqual(actual._, ['true.txt']); }); }); diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index f344b73b2e..7fe73e79b5 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -37,11 +37,6 @@ suite('Native Modules (all platforms)', () => { assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog')); }); - test('v8-inspect-profiler', async () => { - const profiler = await import('v8-inspect-profiler'); - assert.ok(typeof profiler.startProfiling === 'function', testErrorMessage('v8-inspect-profiler')); - }); - test('vscode-nsfw', async () => { const nsfWatcher = await import('vscode-nsfw'); assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw')); @@ -90,7 +85,7 @@ suite('Native Modules (all platforms)', () => { test('vscode-windows-ca-certs', async () => { // @ts-ignore Windows only const windowsCerts = await import('vscode-windows-ca-certs'); - const store = windowsCerts(); + const store = new windowsCerts.Crypt32(); assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs')); let certCount = 0; try { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index f768c75abd..dc2c1fed55 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -152,6 +152,12 @@ const DefaultQueryState: IQueryState = { assetTypes: [] }; +type QueryTelemetryData = { + filterTypes: string[]; + sortBy: string; + sortOrder: string; +}; + class Query { constructor(private state = DefaultQueryState) { } @@ -203,6 +209,14 @@ class Query { const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0]; return criterium && criterium.value ? criterium.value : ''; } + + get telemetryData(): QueryTelemetryData { + return { + filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)), + sortBy: String(this.sortBy), + sortOrder: String(this.sortOrder) + }; + } } function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 24ee63a962..a3952eeda9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -282,3 +282,19 @@ export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Ext export const ExtensionsChannelId = 'extensions'; export const PreferencesLabel = localize('preferences', "Preferences"); export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' }; + + +export interface CLIOutput { + log(s: string): void; + error(s: string): void; +} + +export const IExtensionManagementCLIService = createDecorator('IExtensionManagementCLIService'); +export interface IExtensionManagementCLIService { + readonly _serviceBrand: undefined; + + listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise; + installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise; + uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise; + locateExtension(extensions: string[], output?: CLIOutput): Promise; +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts new file mode 100644 index 0000000000..1eddf4575b --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts @@ -0,0 +1,347 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; + +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { gt } from 'vs/base/common/semver/semver'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Schemas } from 'vs/base/common/network'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; + +const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); +const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); + + +function getId(manifest: IExtensionManifest, withVersion?: boolean): string { + if (withVersion) { + return `${manifest.publisher}.${manifest.name}@${manifest.version}`; + } else { + return `${manifest.publisher}.${manifest.name}`; + } +} + +const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; + +export function getIdAndVersion(id: string): [string, string | undefined] { + const matches = EXTENSION_ID_REGEX.exec(id); + if (matches && matches[1]) { + return [adoptToGalleryExtensionId(matches[1]), matches[2]]; + } + return [adoptToGalleryExtensionId(id), undefined]; +} + +type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; + + +export class ExtensionManagementCLIService implements IExtensionManagementCLIService { + + _serviceBrand: any; + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService private readonly localizationsService: ILocalizationsService + ) { } + + protected get location(): string | undefined { + return undefined; + } + + public async listExtensions(showVersions: boolean, category?: string, output: CLIOutput = console): Promise { + let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); + if (category && category !== '') { + if (categories.indexOf(category.toLowerCase()) < 0) { + output.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); + return; + } + extensions = extensions.filter(e => { + if (e.manifest.categories) { + const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); + return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; + } + return false; + }); + } else if (category === '') { + output.log('Possible Categories: '); + categories.forEach(category => { + output.log(category); + }); + return; + } + if (this.location) { + output.log(localize('listFromLocation', "Extensions installed on {0}:", this.location)); + } + + extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id)); + let lastId: string | undefined = undefined; + for (let extension of extensions) { + if (lastId !== extension.identifier.id) { + lastId = extension.identifier.id; + output.log(getId(extension.manifest, showVersions)); + } + } + } + + public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise { + const failed: string[] = []; + const installedExtensionsManifests: IExtensionManifest[] = []; + if (extensions.length) { + output.log(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions...")); + } + + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + const checkIfNotInstalled = (id: string, version?: string): boolean => { + const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); + if (installedExtension) { + if (!version && !force) { + output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); + return false; + } + if (version && installedExtension.manifest.version === version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); + return false; + } + } + return true; + }; + const vsixs: URI[] = []; + const installExtensionInfos: InstallExtensionInfo[] = []; + for (const extension of extensions) { + if (extension instanceof URI) { + vsixs.push(extension); + } else { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); + } + } + } + for (const extension of builtinExtensionIds) { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); + } + } + + if (vsixs.length) { + await Promise.all(vsixs.map(async vsix => { + try { + const manifest = await this.installVSIX(vsix, force, output); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(vsix.toString()); + } + })); + } + + if (installExtensionInfos.length) { + + const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + + await Promise.all(installExtensionInfos.map(async extensionInfo => { + const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); + if (gallery) { + try { + const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force, output); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(extensionInfo.id); + } + } else { + output.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); + failed.push(extensionInfo.id); + } + })); + + } + + if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { + await this.updateLocalizationsCache(); + } + + if (failed.length) { + throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); + } + } + + private async installVSIX(vsix: URI, force: boolean, output: CLIOutput): Promise { + + const manifest = await this.extensionManagementService.getManifest(vsix); + if (!manifest) { + throw new Error('Invalid vsix'); + } + + const valid = await this.validateVSIX(manifest, force, output); + if (valid) { + try { + await this.extensionManagementService.install(vsix); + output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); + return null; + } else { + throw error; + } + } + } + return null; + } + + private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { + const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); + const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); + + const galleryExtensions = new Map(); + await Promise.all([ + (async () => { + const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); + result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); + })(), + Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { + const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); + if (extension) { + galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); + } + })) + ]); + + return galleryExtensions; + } + + private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean, output: CLIOutput): Promise { + const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); + if (manifest && !this.validateExtensionKind(manifest, output)) { + return null; + } + + const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); + if (installedExtension) { + if (galleryExtension.version === installedExtension.manifest.version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); + return null; + } + output.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); + } + + try { + if (installOptions.isBuiltin) { + output.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); + } else { + output.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); + } + + await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); + output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); + return null; + } else { + throw error; + } + } + } + + protected validateExtensionKind(_manifest: IExtensionManifest, output: CLIOutput): boolean { + return true; + } + + private async validateVSIX(manifest: IExtensionManifest, force: boolean, output: CLIOutput): Promise { + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version)); + + if (newer && !force) { + output.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); + return false; + } + + return this.validateExtensionKind(manifest, output); + } + + public async uninstallExtensions(extensions: (string | URI)[], force: boolean, output: CLIOutput = console): Promise { + const getExtensionId = async (extensionDescription: string | URI): Promise => { + if (extensionDescription instanceof URI) { + const manifest = await this.extensionManagementService.getManifest(extensionDescription); + return getId(manifest); + } + return extensionDescription; + }; + + const uninstalledExtensions: ILocalExtension[] = []; + for (const extension of extensions) { + const id = await getExtensionId(extension); + const installed = await this.extensionManagementService.getInstalled(); + const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id })); + if (!extensionsToUninstall.length) { + throw new Error(`${this.notInstalled(id)}\n${useId}`); + } + if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) { + output.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id)); + return; + } + if (!force && extensionsToUninstall.some(e => e.isBuiltin)) { + output.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); + return; + } + output.log(localize('uninstalling', "Uninstalling {0}...", id)); + for (const extensionToUninstall of extensionsToUninstall) { + await this.extensionManagementService.uninstall(extensionToUninstall); + uninstalledExtensions.push(extensionToUninstall); + } + + if (this.location) { + output.log(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location)); + } else { + output.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); + } + + } + + if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { + await this.updateLocalizationsCache(); + } + } + + public async locateExtension(extensions: string[], output: CLIOutput = console): Promise { + const installed = await this.extensionManagementService.getInstalled(); + extensions.forEach(e => { + installed.forEach(i => { + if (i.identifier.id === e) { + if (i.location.scheme === Schemas.file) { + output.log(i.location.fsPath); + return; + } + } + }); + }); + } + + + private updateLocalizationsCache(): Promise { + return this.localizationsService.update(); + } + + private notInstalled(id: string) { + return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id); + } + +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 45b2af4f54..646b5bee0c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -42,12 +42,16 @@ export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithV } } +export function getExtensionId(publisher: string, name: string): string { + return `${publisher}.${name}`; +} + export function adoptToGalleryExtensionId(id: string): string { return id.toLocaleLowerCase(); } export function getGalleryExtensionId(publisher: string, name: string): string { - return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`; + return adoptToGalleryExtensionId(getExtensionId(publisher, name)); } export function groupByExtension(extensions: T[], getExtensionIdentifier: (t: T) => IExtensionIdentifier): T[][] { diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts index dfe16162a2..aacd9f1418 100644 --- a/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/electron-sandbox/extensionTipsService.ts @@ -21,6 +21,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { localize } from 'vs/nls'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Event } from 'vs/base/common/event'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -52,6 +54,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { @ITelemetryService private readonly telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IStorageService private readonly storageService: IStorageService, + @INativeHostService private readonly nativeHostService: INativeHostService, @IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @IFileService fileService: IFileService, @IProductService productService: IProductService, @@ -172,6 +175,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService { case RecommendationsNotificationResult.Ignored: this.highImportanceTipsByExe.delete(exeName); break; + case RecommendationsNotificationResult.IncompatibleWindow: + // Recommended in incompatible window. Schedule the prompt after active window change + const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow))); + this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip())); + break; case RecommendationsNotificationResult.TooMany: // Too many notifications. Schedule the prompt after one hour const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); @@ -217,6 +225,12 @@ export class ExtensionTipsService extends BaseExtensionTipsService { this.promptMediumImportanceExeBasedTip(); break; + case RecommendationsNotificationResult.IncompatibleWindow: + // Recommended in incompatible window. Schedule the prompt after active window change + const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow))); + this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip())); + break; + case RecommendationsNotificationResult.TooMany: // Too many notifications. Schedule the prompt after one hour const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */)); diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 0ef11d7fab..926243f97e 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises } from 'fs'; import { Disposable } from 'vs/base/common/lifecycle'; -import { rename } from 'vs/base/node/pfs'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -15,6 +15,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { generateUuid } from 'vs/base/common/uuid'; import * as semver from 'vs/base/common/semver/semver'; import { isWindows } from 'vs/base/common/platform'; +import { Promises } from 'vs/base/common/async'; const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/; @@ -62,7 +63,7 @@ export class ExtensionsDownloader extends Disposable { private async rename(from: URI, to: URI, retryUntil: number): Promise { try { - await rename(from.fsPath, to.fsPath); + await promises.rename(from.fsPath, to.fsPath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`); @@ -97,7 +98,7 @@ export class ExtensionsDownloader extends Disposable { } distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest - await Promise.all(toDelete.map(resource => { + await Promises.settled(toDelete.map(resource => { this.logService.trace('Deleting vsix from cache', resource.path); return this.fileService.del(resource); })); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 477b8850ed..1f09e09e2d 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; @@ -24,7 +25,7 @@ import { } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; +import { createCancelablePromise, CancelablePromise, Promises } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import * as semver from 'vs/base/common/semver/semver'; import { URI } from 'vs/base/common/uri'; @@ -129,7 +130,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => pfs.stat(e))); + const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; @@ -477,7 +478,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None); const extensionsToInstall = galleryResult.firstPage; try { - await Promise.all(extensionsToInstall.map(e => this.installFromGallery(e, options))); + await Promises.settled(extensionsToInstall.map(e => this.installFromGallery(e, options))); } catch (error) { try { await this.rollback(extensionsToInstall); } catch (e) { /* ignore */ } throw error; @@ -489,7 +490,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private async rollback(extensions: IGalleryExtension[]): Promise { const installed = await this.getInstalled(ExtensionType.User); const extensionsToUninstall = installed.filter(local => extensions.some(galleryExtension => new ExtensionIdentifierWithVersion(local.identifier, local.manifest.version).equals(new ExtensionIdentifierWithVersion(galleryExtension.identifier, galleryExtension.version)))); // Check with version because we want to rollback the exact version - await Promise.all(extensionsToUninstall.map(local => this.uninstall(local))); + await Promises.settled(extensionsToUninstall.map(local => this.uninstall(local))); } async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise { @@ -574,7 +575,7 @@ export class ExtensionManagementService extends Disposable implements IExtension this.checkForDependents(e, extensionsToUninstall, installed, extension); } } - await Promise.all([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]); + await Promises.settled([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]); } private checkForDependents(extension: ILocalExtension, extensionsToUninstall: ILocalExtension[], installed: ILocalExtension[], extensionToUninstall: ILocalExtension): void { diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 2052039295..80ba975dbc 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; import * as semver from 'vs/base/common/semver/semver'; import { Disposable } from 'vs/base/common/lifecycle'; import * as pfs from 'vs/base/node/pfs'; @@ -11,7 +12,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ILocalExtension, IGalleryMetadata, ExtensionManagementError } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions, ExtensionIdentifierWithVersion, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { Limiter, Queue } from 'vs/base/common/async'; +import { Limiter, Promises, Queue } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; @@ -138,7 +139,7 @@ export class ExtensionsScanner extends Disposable { metadata.isMachineScoped = metadata.isMachineScoped || undefined; metadata.isBuiltin = metadata.isBuiltin || undefined; const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await pfs.readFile(manifestPath, 'utf8'); + const raw = await fs.promises.readFile(manifestPath, 'utf8'); const { manifest } = await this.parseManifest(raw); (manifest as ILocalExtensionManifest).__metadata = metadata; await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); @@ -153,7 +154,7 @@ export class ExtensionsScanner extends Disposable { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - raw = await pfs.readFile(this.uninstalledPath, 'utf8'); + raw = await fs.promises.readFile(this.uninstalledPath, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') { throw err; @@ -211,7 +212,7 @@ export class ExtensionsScanner extends Disposable { private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { try { - await pfs.rename(extractPath, renamePath); + await fs.promises.rename(extractPath, renamePath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); @@ -300,14 +301,14 @@ export class ExtensionsScanner extends Disposable { } } const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - await Promise.all(byExtension.map(async e => { + await Promises.settled(byExtension.map(async e => { const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; if (!installed.has(latest.identifier.id.toLowerCase())) { await this.beforeRemovingExtension(latest); } })); const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); - await Promise.all(toRemove.map(e => this.removeUninstalledExtension(e))); + await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); } private async removeOutdatedExtensions(): Promise { @@ -318,7 +319,7 @@ export class ExtensionsScanner extends Disposable { const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1)))); - await Promise.all(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); + await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); } private getDevSystemExtensionsList(): string[] { @@ -345,9 +346,9 @@ export class ExtensionsScanner extends Disposable { private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> { const promises = [ - pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') + fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') .then(raw => this.parseManifest(raw)), - pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') + fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') .then(raw => JSON.parse(raw)) ]; diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts new file mode 100644 index 0000000000..a4b124df9c --- /dev/null +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { isUUID } from 'vs/base/common/uuid'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { mock } from 'vs/base/test/common/mock'; + +class EnvironmentServiceMock extends mock() { + constructor(readonly serviceMachineIdResource: URI) { + super(); + } +} + +suite('Extension Gallery Service', () => { + const disposables: DisposableStore = new DisposableStore(); + let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService; + + setup(() => { + const serviceMachineIdResource = joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'machineid'); + environmentService = new EnvironmentServiceMock(serviceMachineIdResource); + fileService = disposables.add(new FileService(new NullLogService())); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider); + storageService = new InMemoryStorageService(); + }); + + teardown(() => disposables.clear()); + + test('marketplace machine id', async () => { + const headers = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + assert.ok(isUUID(headers['X-Market-User-Id'])); + const headers2 = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService); + assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); + }); +}); diff --git a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts deleted file mode 100644 index a43d44896d..0000000000 --- a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as os from 'os'; -import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { join } from 'vs/base/common/path'; -import { mkdirp, RimRafMode, rimraf } from 'vs/base/node/pfs'; -import { resolveMarketplaceHeaders, ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; // {{SQL CARBON EDIT}} add imports -import { isUUID } from 'vs/base/common/uuid'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { Schemas } from 'vs/base/common/network'; -import product from 'vs/platform/product/common/product'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { IStorageService } from 'vs/platform/storage/common/storage'; - -suite('Extension Gallery Service', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice'); - const marketplaceHome = join(parentDir, 'Marketplace'); - let fileService: IFileService; - let disposables: DisposableStore; - - setup(done => { - - disposables = new DisposableStore(); - fileService = new FileService(new NullLogService()); - disposables.add(fileService); - - const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); - disposables.add(diskFileSystemProvider); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); - - // Delete any existing backups completely and then re-create it. - rimraf(marketplaceHome, RimRafMode.MOVE).then(() => { - mkdirp(marketplaceHome).then(() => { - done(); - }, error => done(error)); - }, error => done(error)); - }); - - teardown(done => { - disposables.clear(); - rimraf(marketplaceHome, RimRafMode.MOVE).then(done, done); - }); - - test('marketplace machine id', () => { - const args = ['--user-data-dir', marketplaceHome]; - const environmentService = new NativeEnvironmentService(parseArgs(args, OPTIONS)); - const storageService: IStorageService = new TestStorageService(); - - return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers => { - assert.ok(isUUID(headers['X-Market-User-Id'])); - - return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers2 => { - assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); - }); - }); - }); - test('sortByField', () => { // {{SQL CARBON EDIT}} add test - let a: { - extensionId: string | undefined; - extensionName: string | undefined; - displayName: string | undefined; - shortDescription: string | undefined; - publisher: { displayName: string | undefined, publisherId: string | undefined, publisherName: string | undefined; } | undefined; - } = { - extensionId: undefined, - extensionName: undefined, - displayName: undefined, - shortDescription: undefined, - publisher: undefined - }; - let b: { - extensionId: string | undefined; - extensionName: string | undefined; - displayName: string | undefined; - shortDescription: string | undefined; - publisher: { displayName: string | undefined, publisherId: string | undefined, publisherName: string | undefined; } | undefined; - } = { - extensionId: undefined, - extensionName: undefined, - displayName: undefined, - shortDescription: undefined, - publisher: undefined - }; - - - assert.equal(ExtensionGalleryService.compareByField(a.publisher, b.publisher, 'publisherName'), 0); - - a.publisher = { displayName: undefined, publisherId: undefined, publisherName: undefined }; - assert.equal(ExtensionGalleryService.compareByField(a.publisher, b.publisher, 'publisherName'), 1); - - b.publisher = { displayName: undefined, publisherId: undefined, publisherName: undefined }; - assert.equal(ExtensionGalleryService.compareByField(a.publisher, b.publisher, 'publisherName'), 0); - - a.publisher.publisherName = 'a'; - assert.equal(ExtensionGalleryService.compareByField(a.publisher, b.publisher, 'publisherName'), 1); - - b.publisher.publisherName = 'b'; - assert.equal(ExtensionGalleryService.compareByField(a.publisher, b.publisher, 'publisherName'), -1); - - b.publisher.publisherName = 'a'; - assert.equal(ExtensionGalleryService.compareByField(a.publisher, b.publisher, 'publisherName'), 0); - - a.displayName = 'test1'; - assert.equal(ExtensionGalleryService.compareByField(a, b, 'displayName'), 1); - - b.displayName = 'test2'; - assert.equal(ExtensionGalleryService.compareByField(a, b, 'displayName'), -1); - - b.displayName = 'test1'; - assert.equal(ExtensionGalleryService.compareByField(a, b, 'displayName'), 0); - }); - - test('isMatchingExtension', () => { // {{SQL CARBON EDIT}} add test - let createEmptyExtension = () => { - return { - extensionId: '', - extensionName: '', - displayName: '', - shortDescription: '', - publisher: { - displayName: '', - publisherId: '', - publisherName: '' - }, - versions: [], - statistics: [], - flags: '' - }; - }; - let searchText = 'tExt1 withSpace'; - let matchingText = 'test text1 Withspace test'; - let notMatchingText = 'test test'; - let extension; - - assert(!ExtensionGalleryService.isMatchingExtension(undefined, searchText), 'empty extension should not match any search text'); - - extension = createEmptyExtension(); - assert(ExtensionGalleryService.isMatchingExtension(extension, undefined), 'empty search text should match any not null extension'); - - extension = createEmptyExtension(); - extension.extensionName = notMatchingText; - assert(!ExtensionGalleryService.isMatchingExtension(extension, searchText), 'invalid search text should not match extension'); - - extension = createEmptyExtension(); - extension.extensionId = matchingText; - assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'extensionid field should be used for matching'); - - extension = createEmptyExtension(); - extension.extensionName = matchingText; - assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'extensionName field should be used for matching'); - - extension = createEmptyExtension(); - extension.displayName = matchingText; - assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'displayName field should be used for matching'); - - extension = createEmptyExtension(); - extension.shortDescription = matchingText; - assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'shortDescription field should be used for matching'); - - extension = createEmptyExtension(); - extension.publisher.displayName = matchingText; - assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'publisher displayName field should be used for matching'); - - extension = createEmptyExtension(); - extension.publisher.publisherName = matchingText; - assert(ExtensionGalleryService.isMatchingExtension(extension, searchText), 'publisher publisherName field should be used for matching'); - }); -}); diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index d79fee531a..4571de5642 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -11,10 +11,19 @@ export const enum RecommendationSource { EXE = 3 } +export function RecommendationSourceToString(source: RecommendationSource) { + switch (source) { + case RecommendationSource.FILE: return 'file'; + case RecommendationSource.WORKSPACE: return 'workspace'; + case RecommendationSource.EXE: return 'exe'; + } +} + export const enum RecommendationsNotificationResult { Ignored = 'ignored', Cancelled = 'cancelled', TooMany = 'toomany', + IncompatibleWindow = 'incompatibleWindow', Accepted = 'reacted', } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 35a0f3b691..f257f0b117 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -169,8 +169,8 @@ export const EXTENSION_CATEGORIES = [ 'Programming Languages', 'SCM Providers', 'Snippets', - 'Themes', 'Testing', + 'Themes', 'Visualization', 'Other', ]; diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index bf9c5930c5..cf01c5f794 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -8,14 +8,24 @@ import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapab import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { VSBuffer } from 'vs/base/common/buffer'; -import { joinPath, extUri, dirname } from 'vs/base/common/resources'; +import { Throttler } from 'vs/base/common/async'; import { localize } from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; +import { joinPath } from 'vs/base/common/resources'; const INDEXEDDB_VSCODE_DB = 'vscode-web-db'; export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store'; export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store'; +// Standard FS Errors (expected to be thrown in production when invalid FS operations are requested) +const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); +const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); +const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); +const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown); + +// Arbitrary Internal Errors (should never be thrown in production) +const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); + export class IndexedDB { private indexedDBPromise: Promise; @@ -38,7 +48,7 @@ export class IndexedDB { } private openIndexedDB(name: string, version: number, stores: string[]): Promise { - if (browser.isEdge) { + if (browser.isEdgeLegacy) { return Promise.resolve(null); } return new Promise((c, e) => { @@ -65,13 +75,140 @@ export class IndexedDB { }; }); } - } export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability { reset(): Promise; } +type DirEntry = [string, FileType]; + +type IndexedDBFileSystemEntry = + | { + path: string, + type: FileType.Directory, + children: Map, + } + | { + path: string, + type: FileType.File, + size: number | undefined, + }; + +class IndexedDBFileSystemNode { + public type: FileType; + + constructor(private entry: IndexedDBFileSystemEntry) { + this.type = entry.type; + } + + + read(path: string) { + return this.doRead(path.split('/').filter(p => p.length)); + } + + private doRead(pathParts: string[]): IndexedDBFileSystemEntry | undefined { + if (pathParts.length === 0) { return this.entry; } + if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL('Internal error reading from IndexedDBFSNode -- expected directory at ' + this.entry.path); + } + const next = this.entry.children.get(pathParts[0]); + + if (!next) { return undefined; } + return next.doRead(pathParts.slice(1)); + } + + delete(path: string) { + const toDelete = path.split('/').filter(p => p.length); + if (toDelete.length === 0) { + if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode. Expected root entry to be directory`); + } + this.entry.children.clear(); + } else { + return this.doDelete(toDelete, path); + } + } + + private doDelete = (pathParts: string[], originalPath: string) => { + if (pathParts.length === 0) { + throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode -- got no deletion path parts (encountered while deleting ${originalPath})`); + } + else if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected directory at ' + this.entry.path); + } + else if (pathParts.length === 1) { + this.entry.children.delete(pathParts[0]); + } + else { + const next = this.entry.children.get(pathParts[0]); + if (!next) { + throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected entry at ' + this.entry.path + '/' + next); + } + next.doDelete(pathParts.slice(1), originalPath); + } + }; + + add(path: string, entry: { type: 'file', size?: number } | { type: 'dir' }) { + this.doAdd(path.split('/').filter(p => p.length), entry, path); + } + + private doAdd(pathParts: string[], entry: { type: 'file', size?: number } | { type: 'dir' }, originalPath: string) { + if (pathParts.length === 0) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- adding empty path (encountered while adding ${originalPath})`); + } + else if (this.entry.type !== FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- parent is not a directory (encountered while adding ${originalPath})`); + } + else if (pathParts.length === 1) { + const next = pathParts[0]; + const existing = this.entry.children.get(next); + if (entry.type === 'dir') { + if (existing?.entry.type === FileType.File) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + this.entry.children.set(next, existing ?? new IndexedDBFileSystemNode({ + type: FileType.Directory, + path: this.entry.path + '/' + next, + children: new Map(), + })); + } else { + if (existing?.entry.type === FileType.Directory) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting directory with file: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + this.entry.children.set(next, new IndexedDBFileSystemNode({ + type: FileType.File, + path: this.entry.path + '/' + next, + size: entry.size, + })); + } + } + else if (pathParts.length > 1) { + const next = pathParts[0]; + let childNode = this.entry.children.get(next); + if (!childNode) { + childNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: this.entry.path + '/' + next, + type: FileType.Directory + }); + this.entry.children.set(next, childNode); + } + else if (childNode.type === FileType.File) { + throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file entry with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`); + } + childNode.doAdd(pathParts.slice(1), entry, originalPath); + } + } + + print(indentation = '') { + console.log(indentation + this.entry.path); + if (this.entry.type === FileType.Directory) { + this.entry.children.forEach(child => child.print(indentation + ' ')); + } + } +} + class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities = @@ -83,11 +220,14 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy readonly onDidChangeFile: Event = this._onDidChangeFile.event; private readonly versions: Map = new Map(); - private readonly dirs: Set = new Set(); - constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) { + private cachedFiletree: Promise | undefined; + private writeManyThrottler: Throttler; + + constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) { super(); - this.dirs.add('/'); + this.writeManyThrottler = new Throttler(); + } watch(resource: URI, opts: IWatchOptions): IDisposable { @@ -98,29 +238,22 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy try { const resourceStat = await this.stat(resource); if (resourceStat.type === FileType.File) { - throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); + throw ERR_FILE_NOT_DIR; } } catch (error) { /* Ignore */ } - - // Make sure parent dir exists - await this.stat(dirname(resource)); - - this.dirs.add(resource.path); + (await this.getFiletree()).add(resource.path, { type: 'dir' }); } async stat(resource: URI): Promise { - try { - const content = await this.readFile(resource); + const content = (await this.getFiletree()).read(resource.path); + if (content?.type === FileType.File) { return { type: FileType.File, ctime: 0, mtime: this.versions.get(resource.toString()) || 0, - size: content.byteLength + size: content.size ?? (await this.readFile(resource)).byteLength }; - } catch (e) { - } - const files = await this.readdir(resource); - if (files.length) { + } else if (content?.type === FileType.Directory) { return { type: FileType.Directory, ctime: 0, @@ -128,75 +261,112 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy size: 0 }; } - if (this.dirs.has(resource.path)) { - return { - type: FileType.Directory, - ctime: 0, - mtime: 0, - size: 0 - }; + else { + throw ERR_FILE_NOT_FOUND; } - throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); } - async readdir(resource: URI): Promise<[string, FileType][]> { - const hasKey = await this.hasKey(resource.path); - if (hasKey) { - throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); + async readdir(resource: URI): Promise { + const entry = (await this.getFiletree()).read(resource.path); + if (!entry) { + // Dirs aren't saved to disk, so empty dirs will be lost on reload. + // Thus we have two options for what happens when you try to read a dir and nothing is found: + // - Throw FileSystemProviderErrorCode.FileNotFound + // - Return [] + // We choose to return [] as creating a dir then reading it (even after reload) should not throw an error. + return []; } - const keys = await this.getAllKeys(); - const files: Map = new Map(); - for (const key of keys) { - const keyResource = this.toResource(key); - if (extUri.isEqualOrParent(keyResource, resource)) { - const path = extUri.relativePath(resource, keyResource); - if (path) { - const keySegments = path.split('/'); - files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]); - } - } + if (entry.type !== FileType.Directory) { + throw ERR_FILE_NOT_DIR; + } + else { + return [...entry.children.entries()].map(([name, node]) => [name, node.type]); } - return [...files.values()]; } async readFile(resource: URI): Promise { - const hasKey = await this.hasKey(resource.path); - if (!hasKey) { - throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound); - } - const value = await this.getValue(resource.path); - if (typeof value === 'string') { - return VSBuffer.fromString(value).buffer; - } else { - return value; - } + const buffer = await new Promise((c, e) => { + const transaction = this.database.transaction([this.store]); + const objectStore = transaction.objectStore(this.store); + const request = objectStore.get(resource.path); + request.onerror = () => e(request.error); + request.onsuccess = () => { + if (request.result instanceof Uint8Array) { + c(request.result); + } else if (typeof request.result === 'string') { + c(VSBuffer.fromString(request.result).buffer); + } + else { + if (request.result === undefined) { + e(ERR_FILE_NOT_FOUND); + } else { + e(ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`)); + } + } + }; + }); + + (await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength }); + return buffer; } async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - const hasKey = await this.hasKey(resource.path); - if (!hasKey) { - const files = await this.readdir(resource); - if (files.length) { - throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); - } + const existing = await this.stat(resource).catch(() => undefined); + if (existing?.type === FileType.Directory) { + throw ERR_FILE_IS_DIR; } - await this.setValue(resource.path, content); + + this.fileWriteBatch.push({ content, resource }); + await this.writeManyThrottler.queue(() => this.writeMany()); + (await this.getFiletree()).add(resource.path, { type: 'file', size: content.byteLength }); this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1); this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]); } async delete(resource: URI, opts: FileDeleteOptions): Promise { - const hasKey = await this.hasKey(resource.path); - if (hasKey) { - await this.deleteKey(resource.path); - this.versions.delete(resource.path); - this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]); - return; + let stat: IStat; + try { + stat = await this.stat(resource); + } catch (e) { + if (e.code === FileSystemProviderErrorCode.FileNotFound) { + return; + } + throw e; } + let toDelete: string[]; if (opts.recursive) { - const files = await this.readdir(resource); - await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts))); + const tree = (await this.tree(resource)); + toDelete = tree.map(([path]) => path); + } else { + if (stat.type === FileType.Directory && (await this.readdir(resource)).length) { + throw ERR_DIR_NOT_EMPTY; + } + toDelete = [resource.path]; + } + await this.deleteKeys(toDelete); + (await this.getFiletree()).delete(resource.path); + toDelete.forEach(key => this.versions.delete(key)); + this._onDidChangeFile.fire(toDelete.map(path => ({ resource: resource.with({ path }), type: FileChangeType.DELETED }))); + } + + private async tree(resource: URI): Promise { + if ((await this.stat(resource)).type === FileType.Directory) { + const topLevelEntries = (await this.readdir(resource)).map(([key, type]) => { + return [joinPath(resource, key).path, type] as [string, FileType]; + }); + let allEntries = topLevelEntries; + await Promise.all(topLevelEntries.map( + async ([key, type]) => { + if (type === FileType.Directory) { + const childEntries = (await this.tree(resource.with({ path: key }))); + allEntries = allEntries.concat(childEntries); + } + })); + return allEntries; + } else { + const entries: DirEntry[] = [[resource.path, FileType.File]]; + return entries; } } @@ -204,58 +374,57 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy return Promise.reject(new Error('Not Supported')); } - private toResource(key: string): URI { - return URI.file(key).with({ scheme: this.scheme }); + private getFiletree(): Promise { + if (!this.cachedFiletree) { + this.cachedFiletree = new Promise((c, e) => { + const transaction = this.database.transaction([this.store]); + const objectStore = transaction.objectStore(this.store); + const request = objectStore.getAllKeys(); + request.onerror = () => e(request.error); + request.onsuccess = () => { + const rootNode = new IndexedDBFileSystemNode({ + children: new Map(), + path: '', + type: FileType.Directory + }); + const keys = request.result.map(key => key.toString()); + keys.forEach(key => rootNode.add(key, { type: 'file' })); + c(rootNode); + }; + }); + } + return this.cachedFiletree; } - async getAllKeys(): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getAllKeys(); - request.onerror = () => e(request.error); - request.onsuccess = () => c(request.result); - }); - } + private fileWriteBatch: { resource: URI, content: Uint8Array }[] = []; + private async writeMany() { + return new Promise((c, e) => { + const fileBatch = this.fileWriteBatch; + this.fileWriteBatch = []; + if (fileBatch.length === 0) { return c(); } - hasKey(key: string): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.getKey(key); - request.onerror = () => e(request.error); - request.onsuccess = () => { - c(!!request.result); - }; - }); - } - - getValue(key: string): Promise { - return new Promise(async (c, e) => { - const transaction = this.database.transaction([this.store]); - const objectStore = transaction.objectStore(this.store); - const request = objectStore.get(key); - request.onerror = () => e(request.error); - request.onsuccess = () => c(request.result || ''); - }); - } - - setValue(key: string, value: Uint8Array): Promise { - return new Promise(async (c, e) => { const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - const request = objectStore.put(value, key); - request.onerror = () => e(request.error); + let request: IDBRequest = undefined!; + for (const entry of fileBatch) { + request = objectStore.put(entry.content, entry.resource.path); + } request.onsuccess = () => c(); }); } - deleteKey(key: string): Promise { + private deleteKeys(keys: string[]): Promise { return new Promise(async (c, e) => { + if (keys.length === 0) { return c(); } const transaction = this.database.transaction([this.store], 'readwrite'); + transaction.onerror = () => e(transaction.error); const objectStore = transaction.objectStore(this.store); - const request = objectStore.delete(key); - request.onerror = () => e(request.error); + let request: IDBRequest = undefined!; + for (const key of keys) { + request = objectStore.delete(key); + } + request.onsuccess = () => c(); }); } diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 3684798d10..a02ae00ff8 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -3,19 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { mark } from 'vs/base/common/performance'; import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { isAbsolutePath, dirname, basename, joinPath, IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; -import { localize } from 'vs/nls'; +import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources'; import { TernarySearchTree } from 'vs/base/common/map'; import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; -import { getBaseLabel } from 'vs/base/common/labels'; import { ILogService } from 'vs/platform/log/common/log'; -import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; -import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream } from 'vs/base/common/stream'; -import { Queue } from 'vs/base/common/async'; +import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; +import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream'; +import { Promises, Queue } from 'vs/base/common/async'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; import { readFileIntoStream } from 'vs/platform/files/common/io'; @@ -49,6 +49,8 @@ export class FileService extends Disposable implements IFileService { throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`); } + mark(`code/registerFilesystem/${scheme}`); + // Add provider with event this.provider.set(scheme, provider); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); @@ -89,7 +91,7 @@ export class FileService extends Disposable implements IFileService { // If the provider is not yet there, make sure to join on the listeners assuming // that it takes a bit longer to register the file system provider. - await Promise.all(joiners); + await Promises.settled(joiners); } canHandleResource(resource: URI): boolean { @@ -102,7 +104,7 @@ export class FileService extends Disposable implements IFileService { return !!(provider && (provider.capabilities & capability)); } - listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }> { + listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities; }> { return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities })); } @@ -215,14 +217,15 @@ export class FileService extends Disposable implements IFileService { }); } - private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise; - private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise { + const { providerExtUri } = this.getExtUri(provider); // convert to file stat const fileStat: IFileStat = { resource, - name: getBaseLabel(resource), + name: providerExtUri.basename(resource), isFile: (stat.type & FileType.File) !== 0, isDirectory: (stat.type & FileType.Directory) !== 0, isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, @@ -236,9 +239,9 @@ export class FileService extends Disposable implements IFileService { if (fileStat.isDirectory && recurse(fileStat, siblings)) { try { const entries = await provider.readdir(resource); - const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => { + const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => { try { - const childResource = joinPath(resource, name); + const childResource = providerExtUri.joinPath(resource, name); const childStat = resolveMetadata ? await provider.stat(childResource) : { type }; return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); @@ -263,10 +266,10 @@ export class FileService extends Disposable implements IFileService { return fileStat; } - async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise; - async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise; + async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions; }[]): Promise; + async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions; }[]): Promise; async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { - return Promise.all(toResolve.map(async entry => { + return Promises.settled(toResolve.map(async entry => { try { return { stat: await this.doResolveFile(entry.resource, entry.options), success: true }; } catch (error) { @@ -327,6 +330,7 @@ export class FileService extends Disposable implements IFileService { async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); + const { providerExtUri } = this.getExtUri(provider); try { @@ -335,7 +339,7 @@ export class FileService extends Disposable implements IFileService { // mkdir recursively as needed if (!stat) { - await this.mkdirp(provider, dirname(resource)); + await this.mkdirp(provider, providerExtUri.dirname(resource)); } // optimization: if the provider has unbuffered write capability and the data @@ -435,7 +439,7 @@ export class FileService extends Disposable implements IFileService { return this.doReadAsFileStream(provider, resource, options); } - private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean }): Promise { + private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise { // install a cancellation token that gets cancelled // when any error occurs. this allows us to resolve @@ -450,6 +454,8 @@ export class FileService extends Disposable implements IFileService { throw error; }); + let fileStreamObserver: IReadableStreamObservable | undefined = undefined; + try { // if the etag is provided, we await the result of the validation @@ -460,30 +466,41 @@ export class FileService extends Disposable implements IFileService { await statPromise; } - let fileStreamPromise: Promise; + let fileStream: VSBufferReadableStream | undefined = undefined; // read unbuffered (only if either preferred, or the provider has no buffered read capability) if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) { - fileStreamPromise = this.readFileUnbuffered(provider, resource, options); + fileStream = this.readFileUnbuffered(provider, resource, options); } // read streamed (always prefer over primitive buffered read) else if (hasFileReadStreamCapability(provider)) { - fileStreamPromise = Promise.resolve(this.readFileStreamed(provider, resource, cancellableSource.token, options)); + fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, options); } // read buffered else { - fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options)); + fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options); } - const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]); + // observe the stream for the error case below + fileStreamObserver = observe(fileStream); + + const fileStat = await statPromise; return { ...fileStat, value: fileStream }; } catch (error) { + + // Await the stream to finish so that we exit this method + // in a consistent state with file handles closed + // (https://github.com/microsoft/vscode/issues/114024) + if (fileStreamObserver) { + await fileStreamObserver.errorOrEnd(); + } + throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); } } @@ -509,23 +526,36 @@ export class FileService extends Disposable implements IFileService { return stream; } - private async readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): Promise { - let buffer = await provider.readFile(resource); + private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream { + const stream = newWriteableStream(data => VSBuffer.concat(data)); - // respect position option - if (options && typeof options.position === 'number') { - buffer = buffer.slice(options.position); - } + // Read the file into the stream async but do not wait for + // this to complete because streams work via events + (async () => { + try { + let buffer = await provider.readFile(resource); - // respect length option - if (options && typeof options.length === 'number') { - buffer = buffer.slice(0, options.length); - } + // respect position option + if (options && typeof options.position === 'number') { + buffer = buffer.slice(options.position); + } - // Throw if file is too large to load - this.validateReadFileLimits(resource, buffer.byteLength, options); + // respect length option + if (options && typeof options.length === 'number') { + buffer = buffer.slice(0, options.length); + } - return bufferToStream(VSBuffer.wrap(buffer)); + // Throw if file is too large to load + this.validateReadFileLimits(resource, buffer.byteLength, options); + + // End stream with data + stream.end(VSBuffer.wrap(buffer)); + } catch (err) { + stream.error(err); + } + })(); + + return stream; } private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise { @@ -634,7 +664,7 @@ export class FileService extends Disposable implements IFileService { } // create parent folders - await this.mkdirp(targetProvider, dirname(target)); + await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target)); // copy source => target if (mode === 'copy') { @@ -671,7 +701,6 @@ export class FileService extends Disposable implements IFileService { // across providers: copy to target & delete at source else { await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); - await this.del(source, { recursive: true }); return 'copy'; @@ -709,8 +738,8 @@ export class FileService extends Disposable implements IFileService { // create children in target if (Array.isArray(sourceFolder.children)) { - await Promise.all(sourceFolder.children.map(async sourceChild => { - const targetChild = joinPath(targetFolder, sourceChild.name); + await Promises.settled(sourceFolder.children.map(async sourceChild => { + const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name); if (sourceChild.isDirectory) { return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild); } else { @@ -720,21 +749,21 @@ export class FileService extends Disposable implements IFileService { } } - private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean }> { + private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean; }> { let isSameResourceWithDifferentPathCase = false; // Check if source is equal or parent to target (requires providers to be the same) if (sourceProvider === targetProvider) { - const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); + const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); if (!isPathCaseSensitive) { - isSameResourceWithDifferentPathCase = extUri.isEqual(source, target); + isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target); } if (isSameResourceWithDifferentPathCase && mode === 'copy') { throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target))); } - if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) { + if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) { throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } @@ -751,8 +780,8 @@ export class FileService extends Disposable implements IFileService { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider) { - const { extUri } = this.getExtUri(sourceProvider); - if (extUri.isEqualOrParent(source, target)) { + const { providerExtUri } = this.getExtUri(sourceProvider); + if (providerExtUri.isEqualOrParent(source, target)) { throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target))); } } @@ -761,11 +790,11 @@ export class FileService extends Disposable implements IFileService { return { exists, isSameResourceWithDifferentPathCase }; } - private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } { + private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri, isPathCaseSensitive: boolean; } { const isPathCaseSensitive = this.isPathCaseSensitive(provider); return { - extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, + providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, isPathCaseSensitive }; } @@ -791,8 +820,8 @@ export class FileService extends Disposable implements IFileService { const directoriesToCreate: string[] = []; // mkdir until we reach root - const { extUri } = this.getExtUri(provider); - while (!extUri.isEqual(directory, dirname(directory))) { + const { providerExtUri } = this.getExtUri(provider); + while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { @@ -808,16 +837,16 @@ export class FileService extends Disposable implements IFileService { } // Upon error, remember directories that need to be created - directoriesToCreate.push(basename(directory)); + directoriesToCreate.push(providerExtUri.basename(directory)); // Continue up - directory = dirname(directory); + directory = providerExtUri.dirname(directory); } } // Create directories as needed for (let i = directoriesToCreate.length - 1; i >= 0; i--) { - directory = joinPath(directory, directoriesToCreate[i]); + directory = providerExtUri.joinPath(directory, directoriesToCreate[i]); try { await provider.mkdir(directory); @@ -894,11 +923,11 @@ export class FileService extends Disposable implements IFileService { private readonly _onDidFilesChange = this._register(new Emitter()); readonly onDidFilesChange = this._onDidFilesChange.event; - private readonly activeWatchers = new Map(); + private readonly activeWatchers = new Map(); watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable { let watchDisposed = false; - let watchDisposable = toDisposable(() => watchDisposed = true); + let disposeWatch = () => { watchDisposed = true; }; // Watch and wire in disposable which is async but // check if we got disposed meanwhile and forward @@ -906,11 +935,11 @@ export class FileService extends Disposable implements IFileService { if (watchDisposed) { dispose(disposable); } else { - watchDisposable = disposable; + disposeWatch = () => dispose(disposable); } }, error => this.logService.error(error)); - return toDisposable(() => dispose(watchDisposable)); + return toDisposable(() => disposeWatch()); } async doWatch(resource: URI, options: IWatchOptions): Promise { @@ -940,12 +969,12 @@ export class FileService extends Disposable implements IFileService { } private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { - const { extUri } = this.getExtUri(provider); + const { providerExtUri } = this.getExtUri(provider); return [ - extUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive - String(options.recursive), // use recursive: true | false as part of the key - options.excludes.join() // use excludes as part of the key + providerExtUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key ].join(); } @@ -963,8 +992,8 @@ export class FileService extends Disposable implements IFileService { private readonly writeQueues: Map> = new Map(); private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue { - const { extUri } = this.getExtUri(provider); - const queueKey = extUri.getComparisonKey(resource); + const { providerExtUri } = this.getExtUri(provider); + const queueKey = providerExtUri.getComparisonKey(resource); // ensure to never write to the same resource without finishing // the one write. this ensures a write finishes consistently @@ -1141,7 +1170,7 @@ export class FileService extends Disposable implements IFileService { } catch (error) { throw ensureFileSystemProviderError(error); } finally { - await Promise.all([ + await Promises.settled([ typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(), typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(), ]); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index b1dbeb3ffa..34997c853c 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -278,7 +278,7 @@ export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event; - readonly onDidErrorOccur?: Event; // TODO@ben remove once file watchers are solid + readonly onDidErrorOccur?: Event; // TODO@bpasero remove once file watchers are solid readonly onDidChangeFile: Event; watch(resource: URI, opts: IWatchOptions): IDisposable; @@ -947,9 +947,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined return stat.mtime.toString(29) + stat.size.toString(31); } -export function whenProviderRegistered(file: URI, fileService: IFileService): Promise { +export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise { if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { - return Promise.resolve(); + return; } return new Promise(resolve => { diff --git a/src/vs/platform/files/common/io.ts b/src/vs/platform/files/common/io.ts index c28301392b..0faa3ea277 100644 --- a/src/vs/platform/files/common/io.ts +++ b/src/vs/platform/files/common/io.ts @@ -58,10 +58,11 @@ async function doReadFileIntoStream(provider: IFileSystemProviderWithOpenRead // open handle through provider const handle = await provider.open(resource, { create: false }); - // Check for cancellation - throwIfCancelled(token); - try { + + // Check for cancellation + throwIfCancelled(token); + let totalBytesRead = 0; let bytesRead = 0; let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined; diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index edb4913b5c..e907ce42ed 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs'; +import { open, close, read, write, fdatasync, Stats, promises } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs'; +import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; @@ -80,7 +80,7 @@ export class DiskFileSystemProvider extends Disposable implements async stat(resource: URI): Promise { try { - const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly + const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly return { type: this.toType(stat, symbolicLink), @@ -95,7 +95,7 @@ export class DiskFileSystemProvider extends Disposable implements async readdir(resource: URI): Promise<[string, FileType][]> { try { - const children = await readdirWithFileTypes(this.toFilePath(resource)); + const children = await readdir(this.toFilePath(resource), { withFileTypes: true }); const result: [string, FileType][] = []; await Promise.all(children.map(async child => { @@ -119,7 +119,7 @@ export class DiskFileSystemProvider extends Disposable implements } } - private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType { + private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType { // Signal file type by checking for file / directory, except: // - symbolic links pointing to non-existing files are FileType.Unknown @@ -151,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const filePath = this.toFilePath(resource); - return await readFile(filePath); + return await promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -212,18 +212,20 @@ export class DiskFileSystemProvider extends Disposable implements let flags: string | undefined = undefined; if (opts.create) { - if (isWindows && await exists(filePath)) { + if (isWindows) { try { // On Windows and if the file exists, we use a different strategy of saving the file // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await truncate(filePath, 0); + await promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; } catch (error) { - this.logService.trace(error); + if (error.code !== 'ENOENT') { + this.logService.trace(error); + } } } @@ -398,7 +400,7 @@ export class DiskFileSystemProvider extends Disposable implements async mkdir(resource: URI): Promise { try { - await promisify(mkdir)(this.toFilePath(resource)); + await promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -418,7 +420,7 @@ export class DiskFileSystemProvider extends Disposable implements if (opts.recursive) { await rimraf(filePath, RimRafMode.MOVE); } else { - await unlink(filePath); + await promises.unlink(filePath); } } @@ -463,7 +465,7 @@ export class DiskFileSystemProvider extends Disposable implements await this.validateTargetDeleted(from, to, 'copy', opts.overwrite); // Copy - await copy(fromFilePath, toFilePath); + await copy(fromFilePath, toFilePath, { preserveSymlinks: true }); } catch (error) { // rewrite some typical errors that can happen especially around symlinks @@ -522,7 +524,7 @@ export class DiskFileSystemProvider extends Disposable implements return this.watchRecursive(resource, opts.excludes); } - return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases + return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases } private watchRecursive(resource: URI, excludes: string[]): IDisposable { diff --git a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts index f2d7b1988b..17e688af6a 100644 --- a/src/vs/platform/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nodejs/watcherService.ts @@ -5,7 +5,7 @@ import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { Disposable } from 'vs/base/common/lifecycle'; -import { statLink } from 'vs/base/node/pfs'; +import { SymlinkSupport } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -35,7 +35,7 @@ export class FileWatcher extends Disposable { private async startWatching(): Promise { try { - const { stat, symbolicLink } = await statLink(this.path); + const { stat, symbolicLink } = await SymlinkSupport.stat(this.path); if (this.isDisposed) { return; @@ -47,6 +47,10 @@ export class FileWatcher extends Disposable { pathToWatch = await realpath(pathToWatch); } catch (error) { this.onError(error); + + if (symbolicLink.dangling) { + return; // give up if symbolic link is dangling + } } } @@ -70,7 +74,9 @@ export class FileWatcher extends Disposable { }, error => this.onError(error))); } } catch (error) { - this.onError(error); + if (error.code !== 'ENOENT') { + this.onError(error); + } } } diff --git a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts index db11654b7c..b86759ad9f 100644 --- a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as glob from 'vs/base/common/glob'; -import * as extpath from 'vs/base/common/extpath'; -import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; -import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import * as nsfw from 'vscode-nsfw'; +import * as glob from 'vs/base/common/glob'; +import { join } from 'vs/base/common/path'; +import { isMacintosh } from 'vs/base/common/platform'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -111,7 +111,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // We have to detect this case and massage the events to correct this. let realBasePathDiffers = false; let realBasePathLength = request.path.length; - if (platform.isMacintosh) { + if (isMacintosh) { try { // First check for symbolic link @@ -141,7 +141,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { for (const e of events) { // Logging if (this.verboseLogging) { - const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || ''); + const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || ''); this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); } @@ -149,20 +149,20 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { let absolutePath: string; if (e.action === nsfw.actions.RENAMED) { // Rename fires when a file's name changes within a single directory - absolutePath = path.join(e.directory, e.oldFile || ''); + absolutePath = join(e.directory, e.oldFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } - absolutePath = path.join(e.newDirectory || e.directory, e.newFile || ''); + absolutePath = join(e.newDirectory || e.directory, e.newFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } } else { - absolutePath = path.join(e.directory, e.file || ''); + absolutePath = join(e.directory, e.file || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: nsfwActionToRawChangeType[e.action], @@ -179,7 +179,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { const events = undeliveredFileEvents; undeliveredFileEvents = []; - if (platform.isMacintosh) { + if (isMacintosh) { events.forEach(e => { // Mac uses NFD unicode form on disk, but we want NFC @@ -230,7 +230,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // Normalizes a set of root paths by removing any root paths that are // sub-paths of other roots. return roots.filter(r => roots.every(other => { - return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path)); + return !(r.path.length > other.path.length && isEqualOrParent(r.path, other.path)); })); } diff --git a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts index 1cbf7b5209..7db8e2a735 100644 --- a/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts +++ b/src/vs/platform/files/node/watcher/nsfw/test/nsfwWatcherService.test.ts @@ -30,28 +30,28 @@ suite('NSFW Watcher Service', async () => { test('should not impacts roots that don\'t overlap', () => { const service = new TestNsfwWatcherService(); if (platform.isWindows) { - assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else { - assert.deepEqual(service.normalizeRoots(['/a']), ['/a']); - assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); + assert.deepStrictEqual(service.normalizeRoots(['/a']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); } }); test('should remove sub-folders of other roots', () => { const service = new TestNsfwWatcherService(); if (platform.isWindows) { - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); - assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); + assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else { - assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']); - assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); - assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); + assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); } }); }); diff --git a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts index e6336a48b5..e4e697baa9 100644 --- a/src/vs/platform/files/node/watcher/nsfw/watcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/watcherService.ts @@ -39,9 +39,9 @@ export class FileWatcher extends Disposable { serverName: 'File Watcher (nsfw)', args: ['--type=watcherService'], env: { - AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client } } )); diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts index d1d8f50b76..69a29fd6c5 100644 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts @@ -6,9 +6,8 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); -import * as extpath from 'vs/base/common/extpath'; import * as glob from 'vs/base/common/glob'; +import { isEqualOrParent } from 'vs/base/common/extpath'; import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -20,6 +19,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/arrays'; import { Disposable } from 'vs/base/common/lifecycle'; +gracefulFs.gracefulify(fs); // enable gracefulFs + process.noAsar = true; // disable ASAR support in watcher process interface IWatcher { @@ -311,7 +312,7 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { return false; } - if (extpath.isEqualOrParent(path, request.path)) { + if (isEqualOrParent(path, request.path)) { if (!request.parsedPattern) { if (request.excludes && request.excludes.length > 0) { const pattern = `{${request.excludes.join(',')}}`; @@ -343,7 +344,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string for (const request of requests) { const basePath = request.path; const ignored = (request.excludes || []).sort(); - if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) { + if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) { if (!isEqualIgnore(ignored, prevRequest.excludes)) { result[prevRequest.path].push({ path: basePath, excludes: ignored }); } diff --git a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts index cc748b7a99..5e80847515 100644 --- a/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts +++ b/src/vs/platform/files/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -20,18 +20,18 @@ suite('Chokidar normalizeRoots', async () => { function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { const requests = inputPaths.map(path => newRequest(path)); const actual = normalizeRoots(requests); - assert.deepEqual(Object.keys(actual).sort(), expectedPaths); + assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths); } function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) { const actual = normalizeRoots(inputRequests); const actualPath = Object.keys(actual).sort(); const expectedPaths = Object.keys(expectedRequests).sort(); - assert.deepEqual(actualPath, expectedPaths); + assert.deepStrictEqual(actualPath, expectedPaths); for (let path of actualPath) { let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path)); - assert.deepEqual(a, e); + assert.deepStrictEqual(a, e); } } diff --git a/src/vs/platform/files/node/watcher/unix/watcherService.ts b/src/vs/platform/files/node/watcher/unix/watcherService.ts index 3f407e334c..19d84579f4 100644 --- a/src/vs/platform/files/node/watcher/unix/watcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/watcherService.ts @@ -40,9 +40,9 @@ export class FileWatcher extends Disposable { serverName: 'File Watcher (chokidar)', args: ['--type=watcherService'], env: { - AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', - PIPE_LOGGING: 'true', - VERBOSE_LOGGING: 'true' // transmit console logs from server to client + VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client } } )); diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 19695fc90e..7bc242719a 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -19,7 +19,7 @@ suite('File Service', () => { const resource = URI.parse('test://foo/bar'); const provider = new NullFileSystemProvider(); - assert.equal(service.canHandleResource(resource), false); + assert.strictEqual(service.canHandleResource(resource), false); const registrations: IFileSystemProviderRegistrationEvent[] = []; service.onDidChangeFileSystemProviderRegistrations(e => { @@ -47,33 +47,35 @@ suite('File Service', () => { await service.activateProvider('test'); - assert.equal(service.canHandleResource(resource), true); + assert.strictEqual(service.canHandleResource(resource), true); - assert.equal(registrations.length, 1); - assert.equal(registrations[0].scheme, 'test'); - assert.equal(registrations[0].added, true); + assert.strictEqual(registrations.length, 1); + assert.strictEqual(registrations[0].scheme, 'test'); + assert.strictEqual(registrations[0].added, true); assert.ok(registrationDisposable); - assert.equal(capabilityChanges.length, 0); + assert.strictEqual(capabilityChanges.length, 0); provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy); - assert.equal(capabilityChanges.length, 1); + assert.strictEqual(capabilityChanges.length, 1); provider.setCapabilities(FileSystemProviderCapabilities.Readonly); - assert.equal(capabilityChanges.length, 2); + assert.strictEqual(capabilityChanges.length, 2); await service.activateProvider('test'); - assert.equal(callCount, 2); // activation is called again + assert.strictEqual(callCount, 2); // activation is called again - assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); - assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); + assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true); + assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false); registrationDisposable!.dispose(); - assert.equal(service.canHandleResource(resource), false); + assert.strictEqual(service.canHandleResource(resource), false); - assert.equal(registrations.length, 2); - assert.equal(registrations[1].scheme, 'test'); - assert.equal(registrations[1].added, false); + assert.strictEqual(registrations.length, 2); + assert.strictEqual(registrations[1].scheme, 'test'); + assert.strictEqual(registrations[1].added, false); + + service.dispose(); }); test('watch', async () => { @@ -91,9 +93,9 @@ suite('File Service', () => { const watcher1Disposable = service.watch(resource1); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher1Disposable.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); disposeCounter = 0; const resource2 = URI.parse('test://foo/bar2'); @@ -102,13 +104,13 @@ suite('File Service', () => { const watcher2Disposable3 = service.watch(resource2); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable1.dispose(); - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable2.dispose(); - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher2Disposable3.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); disposeCounter = 0; const resource3 = URI.parse('test://foo/bar3'); @@ -116,10 +118,12 @@ suite('File Service', () => { const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] }); await timeout(0); // service.watch() is async - assert.equal(disposeCounter, 0); + assert.strictEqual(disposeCounter, 0); watcher3Disposable1.dispose(); - assert.equal(disposeCounter, 1); + assert.strictEqual(disposeCounter, 1); watcher3Disposable2.dispose(); - assert.equal(disposeCounter, 2); + assert.strictEqual(disposeCounter, 2); + + service.dispose(); }); }); diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index e8dc9b1015..25d7c731ad 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -6,20 +6,20 @@ import * as assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; -import { posix } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { assertIsDefined } from 'vs/base/common/types'; - -// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s, -// making /-style absolute paths fail isAbsolute checks. -const join = posix.join; +import { basename, joinPath } from 'vs/base/common/resources'; +import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; suite('IndexedDB File Service', function () { + // IDB sometimes under pressure in build machines. + this.retries(3); + const logSchema = 'logs'; let service: FileService; @@ -27,12 +27,43 @@ suite('IndexedDB File Service', function () { let userdataFileProvider: IIndexedDBFileSystemProvider; const testDir = '/'; - const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path }); - const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path }); + const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths); + const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.userData, path: testDir }), ...paths); const disposables = new DisposableStore(); - setup(async () => { + const initFixtures = async () => { + await Promise.all( + [['fixtures', 'resolver', 'examples'], + ['fixtures', 'resolver', 'other', 'deep'], + ['fixtures', 'service', 'deep'], + ['batched']] + .map(path => userdataURIFromPaths(path)) + .map(uri => service.createFolder(uri))); + await Promise.all( + ([ + [['fixtures', 'resolver', 'examples', 'company.js'], 'class company {}'], + [['fixtures', 'resolver', 'examples', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'resolver', 'examples', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'resolver', 'examples', 'small.js'], ''], + [['fixtures', 'resolver', 'other', 'deep', 'company.js'], 'class company {}'], + [['fixtures', 'resolver', 'other', 'deep', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'resolver', 'other', 'deep', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'resolver', 'other', 'deep', 'small.js'], ''], + [['fixtures', 'resolver', 'index.html'], '

p

'], + [['fixtures', 'resolver', 'site.css'], '.p {color: red;}'], + [['fixtures', 'service', 'deep', 'company.js'], 'class company {}'], + [['fixtures', 'service', 'deep', 'conway.js'], 'export function conway() {}'], + [['fixtures', 'service', 'deep', 'employee.js'], 'export const employee = "jax"'], + [['fixtures', 'service', 'deep', 'small.js'], ''], + [['fixtures', 'service', 'binary.txt'], '

p

'], + ] as const) + .map(([path, contents]) => [userdataURIFromPaths(path), contents] as const) + .map(([uri, contents]) => service.createFile(uri, VSBuffer.fromString(contents))) + ); + }; + + const reload = async () => { const logService = new NullLogService(); service = new FileService(logService); @@ -45,33 +76,302 @@ suite('IndexedDB File Service', function () { userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE)); disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider)); disposables.add(userdataFileProvider); + }; + + setup(async () => { + await reload(); }); teardown(async () => { disposables.clear(); + await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false }); + await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + }); - await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false }); - await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false }); + test('root is always present', async () => { + assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory); + await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory); }); test('createFolder', async () => { let event: FileOperationEvent | undefined; disposables.add(service.onDidRunOperation(e => event = e)); - const parent = await service.resolve(makeUserdataURI(testDir)); + const parent = await service.resolve(userdataURIFromPaths([])); + const newFolderResource = joinPath(parent.resource, 'newFolder'); - const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder')); - - assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0); + assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 0); const newFolder = await service.createFolder(newFolderResource); - assert.equal(newFolder.name, 'newFolder'); - // Invalid.. dirs dont exist in our IDBFSB. - // assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1); + assert.strictEqual(newFolder.name, 'newFolder'); + assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 1); + assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory); assert.ok(event); - assert.equal(event!.resource.path, newFolderResource.path); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.path, newFolderResource.path); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.path, newFolderResource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, newFolderResource.path); + assert.strictEqual(event!.target!.isDirectory, true); + }); + + test('createFolder: creating multiple folders at once', async () => { + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const multiFolderPaths = ['a', 'couple', 'of', 'folders']; + const parent = await service.resolve(userdataURIFromPaths([])); + const newFolderResource = joinPath(parent.resource, ...multiFolderPaths); + + const newFolder = await service.createFolder(newFolderResource); + + const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; + assert.strictEqual(newFolder.name, lastFolderName); + assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, newFolderResource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, newFolderResource.path); + assert.strictEqual(event!.target!.isDirectory, true); + }); + + test('exists', async () => { + let exists = await service.exists(userdataURIFromPaths([])); + assert.strictEqual(exists, true); + + exists = await service.exists(userdataURIFromPaths(['hello'])); + assert.strictEqual(exists, false); + }); + + test('resolve - file', async () => { + await initFixtures(); + + const resource = userdataURIFromPaths(['fixtures', 'resolver', 'index.html']); + const resolved = await service.resolve(resource); + + assert.strictEqual(resolved.name, 'index.html'); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, false); + assert.strictEqual(resolved.resource.toString(), resource.toString()); + assert.strictEqual(resolved.children, undefined); + assert.ok(resolved.size! > 0); + }); + + test('resolve - directory', async () => { + await initFixtures(); + + const testsElements = ['examples', 'other', 'index.html', 'site.css']; + + const resource = userdataURIFromPaths(['fixtures', 'resolver']); + const result = await service.resolve(resource); + + assert.ok(result); + assert.strictEqual(result.resource.toString(), resource.toString()); + assert.strictEqual(result.name, 'resolver'); + assert.ok(result.children); + assert.ok(result.children!.length > 0); + assert.ok(result!.isDirectory); + assert.strictEqual(result.children!.length, testsElements.length); + + assert.ok(result.children!.every(entry => { + return testsElements.some(name => { + return basename(entry.resource) === name; + }); + })); + + result.children!.forEach(value => { + assert.ok(basename(value.resource)); + if (['examples', 'other'].indexOf(basename(value.resource)) >= 0) { + assert.ok(value.isDirectory); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else if (basename(value.resource) === 'index.html') { + assert.ok(!value.isDirectory); + assert.ok(!value.children); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else if (basename(value.resource) === 'site.css') { + assert.ok(!value.isDirectory); + assert.ok(!value.children); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); + } else { + assert.ok(!'Unexpected value ' + basename(value.resource)); + } + }); + }); + + test('createFile', async () => { + return assertCreateFile(contents => VSBuffer.fromString(contents)); + }); + + test('createFile (readable)', async () => { + return assertCreateFile(contents => bufferToReadable(VSBuffer.fromString(contents))); + }); + + test('createFile (stream)', async () => { + return assertCreateFile(contents => bufferToStream(VSBuffer.fromString(contents))); + }); + + async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise { + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const contents = 'Hello World'; + const resource = userdataURIFromPaths(['test.txt']); + + assert.strictEqual(await service.canCreateFile(resource), true); + const fileStat = await service.createFile(resource, converter(contents)); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual((await userdataFileProvider.stat(fileStat.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(fileStat.resource)), contents); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, resource.path); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.path, resource.path); + } + + const makeBatchTester = (size: number, name: string) => { + const batch = Array.from({ length: 50 }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) })); + let stats: Promise | undefined = undefined; + return { + async create() { + return stats = Promise.all(batch.map(entry => service.createFile(entry.resource, VSBuffer.fromString(entry.contents)))); + }, + async assertContentsCorrect() { + await Promise.all(batch.map(async (entry, i) => { + if (!stats) { throw Error('read called before create'); } + const stat = (await stats!)[i]; + assert.strictEqual(stat.name, `Hello${i}.txt`); + assert.strictEqual((await userdataFileProvider.stat(stat.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(stat.resource)), entry.contents); + })); + }, + async delete() { + await service.del(userdataURIFromPaths(['batched', name]), { recursive: true, useTrash: false }); + }, + async assertContentsEmpty() { + if (!stats) { throw Error('assertContentsEmpty called before create'); } + await Promise.all((await stats).map(async stat => { + const newStat = await userdataFileProvider.stat(stat.resource).catch(e => e.code); + assert.strictEqual(newStat, FileSystemProviderErrorCode.FileNotFound); + })); + } + }; + }; + + test('createFile (small batch)', async () => { + const tester = makeBatchTester(50, 'smallBatch'); + await tester.create(); + await tester.assertContentsCorrect(); + await tester.delete(); + await tester.assertContentsEmpty(); + }); + + test('createFile (mixed parallel/sequential)', async () => { + const single1 = makeBatchTester(1, 'single1'); + const single2 = makeBatchTester(1, 'single2'); + + const batch1 = makeBatchTester(20, 'batch1'); + const batch2 = makeBatchTester(20, 'batch2'); + + single1.create(); + batch1.create(); + await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); + single2.create(); + batch2.create(); + await Promise.all([single2.assertContentsCorrect(), batch2.assertContentsCorrect()]); + await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); + + await (Promise.all([single1.delete(), single2.delete(), batch1.delete(), batch2.delete()])); + await (Promise.all([single1.assertContentsEmpty(), single2.assertContentsEmpty(), batch1.assertContentsEmpty(), batch2.assertContentsEmpty()])); + }); + + test('deleteFile', async () => { + await initFixtures(); + + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const anotherResource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']); + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']); + const source = await service.resolve(resource); + + assert.strictEqual(await service.canDelete(source.resource, { useTrash: false }), true); + await service.del(source.resource, { useTrash: false }); + + assert.strictEqual(await service.exists(source.resource), false); + assert.strictEqual(await service.exists(anotherResource), true); + + assert.ok(event!); + assert.strictEqual(event!.resource.path, resource.path); + assert.strictEqual(event!.operation, FileOperation.DELETE); + + { + let error: Error | undefined = undefined; + try { + await service.del(source.resource, { useTrash: false }); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + } + await reload(); + { + let error: Error | undefined = undefined; + try { + await service.del(source.resource, { useTrash: false }); + } catch (e) { + error = e; + } + + assert.ok(error); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + } + }); + + test('deleteFolder (recursive)', async () => { + await initFixtures(); + let event: FileOperationEvent; + disposables.add(service.onDidRunOperation(e => event = e)); + + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']); + const subResource1 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']); + const subResource2 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']); + assert.strictEqual(await service.exists(subResource1), true); + assert.strictEqual(await service.exists(subResource2), true); + + const source = await service.resolve(resource); + + assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash: false }), true); + await service.del(source.resource, { recursive: true, useTrash: false }); + + assert.strictEqual(await service.exists(source.resource), false); + assert.strictEqual(await service.exists(subResource1), false); + assert.strictEqual(await service.exists(subResource2), false); + assert.ok(event!); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); + }); + + + test('deleteFolder (non recursive)', async () => { + await initFixtures(); + const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']); + const source = await service.resolve(resource); + + assert.ok((await service.canDelete(source.resource)) instanceof Error); + + let error; + try { + await service.del(source.resource); + } catch (e) { + error = e; + } + assert.ok(error); }); }); diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 7599f76483..afd20090ea 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -9,12 +9,11 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { generateUuid } from 'vs/base/common/uuid'; import { join, basename, dirname, posix } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { copy, rimraf, symlink, RimRafMode, rimrafSync } from 'vs/base/node/pfs'; +import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; +import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { isLinux, isWindows } from 'vs/base/common/platform'; @@ -120,24 +119,16 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occasionally failing tests - const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); const testSchema = 'test'; let service: FileService; let fileProvider: TestDiskFileSystemProvider; let testProvider: TestDiskFileSystemProvider; + let testDir: string; const disposables = new DisposableStore(); - // Given issues such as https://github.com/microsoft/vscode/issues/78602 - // and https://github.com/microsoft/vscode/issues/92334 we see random test - // failures when accessing the native file system. To diagnose further, we - // retry node.js file access tests up to 3 times to rule out any random disk - // issue and increase the timeout. - this.retries(3); - this.timeout(1000 * 10); - setup(async () => { const logService = new NullLogService(); @@ -152,17 +143,17 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ disposables.add(service.registerProvider(testSchema, testProvider)); disposables.add(testProvider); - const id = generateUuid(); - testDir = join(parentDir, id); + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); + const sourceDir = getPathFromAmdModule(require, './fixtures/service'); - await copy(sourceDir, testDir); + await copy(sourceDir, testDir, { preserveSymlinks: false }); }); - teardown(async () => { + teardown(() => { disposables.clear(); - await rimraf(parentDir, RimRafMode.MOVE); + return rimraf(testDir); }); test('createFolder', async () => { @@ -175,14 +166,14 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const newFolder = await service.createFolder(newFolderResource); - assert.equal(newFolder.name, 'newFolder'); - assert.equal(existsSync(newFolder.resource.fsPath), true); + assert.strictEqual(newFolder.name, 'newFolder'); + assert.strictEqual(existsSync(newFolder.resource.fsPath), true); assert.ok(event); - assert.equal(event!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.target!.isDirectory, true); }); test('createFolder: creating multiple folders at once', async () => { @@ -197,34 +188,34 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const newFolder = await service.createFolder(newFolderResource); const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1]; - assert.equal(newFolder.name, lastFolderName); - assert.equal(existsSync(newFolder.resource.fsPath), true); + assert.strictEqual(newFolder.name, lastFolderName); + assert.strictEqual(existsSync(newFolder.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, newFolderResource.fsPath); - assert.equal(event!.target!.isDirectory, true); + assert.strictEqual(event!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, newFolderResource.fsPath); + assert.strictEqual(event!.target!.isDirectory, true); }); test('exists', async () => { let exists = await service.exists(URI.file(testDir)); - assert.equal(exists, true); + assert.strictEqual(exists, true); exists = await service.exists(URI.file(testDir + 'something')); - assert.equal(exists, false); + assert.strictEqual(exists, false); }); test('resolve - file', async () => { const resource = URI.file(getPathFromAmdModule(require, './fixtures/resolver/index.html')); const resolved = await service.resolve(resource); - assert.equal(resolved.name, 'index.html'); - assert.equal(resolved.isFile, true); - assert.equal(resolved.isDirectory, false); - assert.equal(resolved.isSymbolicLink, false); - assert.equal(resolved.resource.toString(), resource.toString()); - assert.equal(resolved.children, undefined); + assert.strictEqual(resolved.name, 'index.html'); + assert.strictEqual(resolved.isFile, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, false); + assert.strictEqual(resolved.resource.toString(), resource.toString()); + assert.strictEqual(resolved.children, undefined); assert.ok(resolved.mtime! > 0); assert.ok(resolved.ctime! > 0); assert.ok(resolved.size! > 0); @@ -237,14 +228,14 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const result = await service.resolve(resource); assert.ok(result); - assert.equal(result.resource.toString(), resource.toString()); - assert.equal(result.name, 'resolver'); + assert.strictEqual(result.resource.toString(), resource.toString()); + assert.strictEqual(result.name, 'resolver'); assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); - assert.equal(result.children!.length, testsElements.length); + assert.strictEqual(result.children!.length, testsElements.length); assert.ok(result.children!.every(entry => { return testsElements.some(name => { @@ -256,18 +247,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(basename(value.resource.fsPath)); if (['examples', 'other'].indexOf(basename(value.resource.fsPath)) >= 0) { assert.ok(value.isDirectory); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else if (basename(value.resource.fsPath) === 'index.html') { assert.ok(!value.isDirectory); assert.ok(!value.children); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else if (basename(value.resource.fsPath) === 'site.css') { assert.ok(!value.isDirectory); assert.ok(!value.children); - assert.equal(value.mtime, undefined); - assert.equal(value.ctime, undefined); + assert.strictEqual(value.mtime, undefined); + assert.strictEqual(value.ctime, undefined); } else { assert.ok(!'Unexpected value ' + basename(value.resource.fsPath)); } @@ -280,13 +271,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const result = await service.resolve(URI.file(getPathFromAmdModule(require, './fixtures/resolver')), { resolveMetadata: true }); assert.ok(result); - assert.equal(result.name, 'resolver'); + assert.strictEqual(result.name, 'resolver'); assert.ok(result.children); assert.ok(result.children!.length > 0); assert.ok(result!.isDirectory); assert.ok(result.mtime! > 0); assert.ok(result.ctime! > 0); - assert.equal(result.children!.length, testsElements.length); + assert.strictEqual(result.children!.length, testsElements.length); assert.ok(result.children!.every(entry => { return testsElements.some(name => { @@ -320,10 +311,10 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ test('resolve - directory with resolveTo', async () => { const resolved = await service.resolve(URI.file(testDir), { resolveTo: [URI.file(join(testDir, 'deep'))] }); - assert.equal(resolved.children!.length, 8); + assert.strictEqual(resolved.children!.length, 8); const deep = (getByName(resolved, 'deep')!); - assert.equal(deep.children!.length, 4); + assert.strictEqual(deep.children!.length, 4); }); test('resolve - directory - resolveTo single directory', async () => { @@ -336,7 +327,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 4); + assert.strictEqual(children.length, 4); const other = getByName(result, 'other'); assert.ok(other); @@ -345,7 +336,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const deep = getByName(other!, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); }); test('resolve directory - resolveTo multiple directories', async () => { @@ -363,7 +354,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 4); + assert.strictEqual(children.length, 4); const other = getByName(result, 'other'); assert.ok(other); @@ -372,12 +363,12 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const deep = getByName(other!, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); const examples = getByName(result, 'examples'); assert.ok(examples); assert.ok(examples!.children!.length > 0); - assert.equal(examples!.children!.length, 4); + assert.strictEqual(examples!.children!.length, 4); }); test('resolve directory - resolveSingleChildFolders', async () => { @@ -390,12 +381,12 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(result.isDirectory); const children = result.children!; - assert.equal(children.length, 1); + assert.strictEqual(children.length, 1); let deep = getByName(result, 'deep'); assert.ok(deep); assert.ok(deep!.children!.length > 0); - assert.equal(deep!.children!.length, 4); + assert.strictEqual(deep!.children!.length, 4); }); test('resolves', async () => { @@ -405,41 +396,41 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ ]); const r1 = (res[0].stat!); - assert.equal(r1.children!.length, 8); + assert.strictEqual(r1.children!.length, 8); const deep = (getByName(r1, 'deep')!); - assert.equal(deep.children!.length, 4); + assert.strictEqual(deep.children!.length, 4); const r2 = (res[1].stat!); - assert.equal(r2.children!.length, 4); - assert.equal(r2.name, 'deep'); + assert.strictEqual(r2.children!.length, 4); + assert.strictEqual(r2.name, 'deep'); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - folder symbolic link', async () => { + test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), link.fsPath); + await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); - assert.equal(resolved.children!.length, 4); - assert.equal(resolved.isDirectory, true); - assert.equal(resolved.isSymbolicLink, true); + assert.strictEqual(resolved.children!.length, 4); + assert.strictEqual(resolved.isDirectory, true); + assert.strictEqual(resolved.isSymbolicLink, true); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - file symbolic link', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await symlink(join(testDir, 'lorem.txt'), link.fsPath); + await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); - assert.equal(resolved.isDirectory, false); - assert.equal(resolved.isSymbolicLink, true); + assert.strictEqual(resolved.isDirectory, false); + assert.strictEqual(resolved.isSymbolicLink, true); }); - (isWindows /* not reliable on windows */ ? test.skip : test)('resolve - symbolic link pointing to non-existing file does not break', async () => { - await symlink(join(testDir, 'foo'), join(testDir, 'bar')); + test('resolve - symbolic link pointing to non-existing file does not break', async () => { + await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); - assert.equal(resolved.isDirectory, true); - assert.equal(resolved.children!.length, 9); + assert.strictEqual(resolved.isDirectory, true); + assert.strictEqual(resolved.children!.length, 9); const resolvedLink = resolved.children?.find(child => child.name === 'bar' && child.isSymbolicLink); assert.ok(resolvedLink); @@ -463,14 +454,14 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolve(resource); - assert.equal(await service.canDelete(source.resource, { useTrash }), true); + assert.strictEqual(await service.canDelete(source.resource, { useTrash }), true); await service.del(source.resource, { useTrash }); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); let error: Error | undefined = undefined; try { @@ -480,47 +471,47 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + assert.strictEqual((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); } - (isWindows /* not reliable on windows */ ? test.skip : test)('deleteFile - symbolic link (exists)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); - assert.equal(await service.canDelete(source.resource), true); + assert.strictEqual(await service.canDelete(source.resource), true); await service.del(source.resource); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, link.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, link.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); - assert.equal(existsSync(target.fsPath), true); // target the link pointed to is never deleted + assert.strictEqual(existsSync(target.fsPath), true); // target the link pointed to is never deleted }); - (isWindows /* not reliable on windows */ ? test.skip : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { + (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); - await symlink(target.fsPath, link.fsPath); + await promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); - assert.equal(await service.canDelete(link), true); + assert.strictEqual(await service.canDelete(link), true); await service.del(link); - assert.equal(existsSync(link.fsPath), false); + assert.strictEqual(existsSync(link.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, link.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, link.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); }); test('deleteFolder (recursive)', async () => { @@ -538,13 +529,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); - assert.equal(await service.canDelete(source.resource, { recursive: true, useTrash }), true); + assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash }), true); await service.del(source.resource, { recursive: true, useTrash }); - assert.equal(existsSync(source.resource.fsPath), false); + assert.strictEqual(existsSync(source.resource.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.DELETE); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.DELETE); } test('deleteFolder (non recursive)', async () => { @@ -572,20 +563,20 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const target = URI.file(join(dirname(source.fsPath), 'other.html')); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); }); test('move - across providers (buffered => buffered)', async () => { @@ -653,20 +644,20 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const target = URI.file(join(dirname(source.fsPath), 'other.html')).with({ scheme: testSchema }); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); } test('move - multi folder', async () => { @@ -678,15 +669,15 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const source = URI.file(join(testDir, 'index.html')); - assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); + assert.strictEqual(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), renameToPath))); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); }); test('move - directory', async () => { @@ -695,15 +686,15 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const source = URI.file(join(testDir, 'deep')); - assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); + assert.strictEqual(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), 'deeper'))); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); }); test('move - directory - across providers (buffered => buffered)', async () => { @@ -743,20 +734,20 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const target = URI.file(join(dirname(source.fsPath), 'deeper')).with({ scheme: testSchema }); - assert.equal(await service.canMove(source, target), true); + assert.strictEqual(await service.canMove(source, target), true); const renamed = await service.move(source, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(existsSync(source.fsPath), false); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(source.fsPath), false); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); const targetChildren = readdirSync(target.fsPath); - assert.equal(sourceChildren.length, targetChildren.length); + assert.strictEqual(sourceChildren.length, targetChildren.length); for (let i = 0; i < sourceChildren.length; i++) { - assert.equal(sourceChildren[i], targetChildren[i]); + assert.strictEqual(sourceChildren[i], targetChildren[i]); } } @@ -768,18 +759,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(source.size > 0); const renamedResource = URI.file(join(dirname(source.resource.fsPath), 'INDEX.html')); - assert.equal(await service.canMove(source.resource, renamedResource), true); + assert.strictEqual(await service.canMove(source.resource, renamedResource), true); let renamed = await service.move(source.resource, renamedResource); - assert.equal(existsSync(renamedResource.fsPath), true); - assert.equal(basename(renamedResource.fsPath), 'INDEX.html'); + assert.strictEqual(existsSync(renamedResource.fsPath), true); + assert.strictEqual(basename(renamedResource.fsPath), 'INDEX.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamedResource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamedResource.fsPath); renamed = await service.resolve(renamedResource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - same file', async () => { @@ -789,18 +780,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); - assert.equal(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); + assert.strictEqual(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); let renamed = await service.move(source.resource, URI.file(source.resource.fsPath)); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(basename(renamed.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); renamed = await service.resolve(renamed.resource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - same file #2', async () => { @@ -813,18 +804,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); - assert.equal(await service.canMove(source.resource, target), true); + assert.strictEqual(await service.canMove(source.resource, target), true); let renamed = await service.move(source.resource, target); - assert.equal(existsSync(renamed.resource.fsPath), true); - assert.equal(basename(renamed.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(basename(renamed.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.MOVE); - assert.equal(event!.target!.resource.fsPath, renamed.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.MOVE); + assert.strictEqual(event!.target!.resource.fsPath, renamed.resource.fsPath); renamed = await service.resolve(renamed.resource, { resolveMetadata: true }); - assert.equal(source.size, renamed.size); + assert.strictEqual(source.size, renamed.size); }); test('move - source parent of target', async () => { @@ -848,7 +839,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(!event!); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); }); test('move - FILE_MOVE_CONFLICT', async () => { @@ -868,11 +859,11 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ error = e; } - assert.equal(error.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); + assert.strictEqual(error.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); assert.ok(!event!); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); }); test('move - overwrite folder with file', async () => { @@ -894,17 +885,17 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); - assert.equal(await service.canMove(source, f.resource, true), true); + assert.strictEqual(await service.canMove(source, f.resource, true), true); const moved = await service.move(source, f.resource, true); - assert.equal(existsSync(moved.resource.fsPath), true); + assert.strictEqual(existsSync(moved.resource.fsPath), true); assert.ok(statSync(moved.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(moveEvent!); - assert.equal(moveEvent!.resource.fsPath, source.fsPath); - assert.equal(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); - assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + assert.strictEqual(moveEvent!.resource.fsPath, source.fsPath); + assert.strictEqual(moveEvent!.target!.resource.fsPath, moved.resource.fsPath); + assert.strictEqual(deleteEvent!.resource.fsPath, folderResource.fsPath); }); test('copy', async () => { @@ -949,21 +940,21 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const source = await service.resolve(URI.file(join(testDir, sourceName))); const target = URI.file(join(testDir, 'other.html')); - assert.equal(await service.canCopy(source.resource, target), true); + assert.strictEqual(await service.canCopy(source.resource, target), true); const copied = await service.copy(source.resource, target); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(existsSync(source.resource.fsPath), true); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(existsSync(source.resource.fsPath), true); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); const sourceContents = readFileSync(source.resource.fsPath); const targetContents = readFileSync(target.fsPath); - assert.equal(sourceContents.byteLength, targetContents.byteLength); - assert.equal(sourceContents.toString(), targetContents.toString()); + assert.strictEqual(sourceContents.byteLength, targetContents.byteLength); + assert.strictEqual(sourceContents.toString(), targetContents.toString()); } test('copy - overwrite folder with file', async () => { @@ -985,17 +976,17 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); - assert.equal(await service.canCopy(source, f.resource, true), true); + assert.strictEqual(await service.canCopy(source, f.resource, true), true); const copied = await service.copy(source, f.resource, true); - assert.equal(existsSync(copied.resource.fsPath), true); + assert.strictEqual(existsSync(copied.resource.fsPath), true); assert.ok(statSync(copied.resource.fsPath).isFile); assert.ok(createEvent!); assert.ok(deleteEvent!); assert.ok(copyEvent!); - assert.equal(copyEvent!.resource.fsPath, source.fsPath); - assert.equal(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); - assert.equal(deleteEvent!.resource.fsPath, folderResource.fsPath); + assert.strictEqual(copyEvent!.resource.fsPath, source.fsPath); + assert.strictEqual(copyEvent!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(deleteEvent!.resource.fsPath, folderResource.fsPath); }); test('copy - MIX CASE same target - no overwrite', async () => { @@ -1017,17 +1008,17 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ if (isLinux) { assert.ok(!error); - assert.equal(canCopy, true); + assert.strictEqual(canCopy, true); - assert.equal(existsSync(copied!.resource.fsPath), true); + assert.strictEqual(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); - assert.equal(source.size, copied!.size); + assert.strictEqual(source.size, copied!.size); } else { assert.ok(error); assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); } }); @@ -1050,17 +1041,17 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ if (isLinux) { assert.ok(!error); - assert.equal(canCopy, true); + assert.strictEqual(canCopy, true); - assert.equal(existsSync(copied!.resource.fsPath), true); + assert.strictEqual(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); - assert.equal(source.size, copied!.size); + assert.strictEqual(source.size, copied!.size); } else { assert.ok(error); assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(originalSize, source.size); + assert.strictEqual(originalSize, source.size); } }); @@ -1069,18 +1060,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(source1.size > 0); const renamed = await service.move(source1.resource, URI.file(join(dirname(source1.resource.fsPath), 'CONWAY.js'))); - assert.equal(existsSync(renamed.resource.fsPath), true); + assert.strictEqual(existsSync(renamed.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'CONWAY.js')); - assert.equal(source1.size, renamed.size); + assert.strictEqual(source1.size, renamed.size); const source2 = await service.resolve(URI.file(join(testDir, 'deep', 'conway.js')), { resolveMetadata: true }); const target = URI.file(join(testDir, basename(source2.resource.path))); - assert.equal(await service.canCopy(source2.resource, target, true), true); + assert.strictEqual(await service.canCopy(source2.resource, target, true), true); const res = await service.copy(source2.resource, target, true); - assert.equal(existsSync(res.resource.fsPath), true); + assert.strictEqual(existsSync(res.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'conway.js')); - assert.equal(source2.size, res.size); + assert.strictEqual(source2.size, res.size); }); test('copy - same file', async () => { @@ -1090,18 +1081,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); - assert.equal(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); + assert.strictEqual(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); let copied = await service.copy(source.resource, URI.file(source.resource.fsPath)); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(basename(copied.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(basename(copied.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); copied = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(source.size, copied.size); + assert.strictEqual(source.size, copied.size); }); test('copy - same file #2', async () => { @@ -1114,18 +1105,18 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); - assert.equal(await service.canCopy(source.resource, URI.file(target.fsPath)), true); + assert.strictEqual(await service.canCopy(source.resource, URI.file(target.fsPath)), true); let copied = await service.copy(source.resource, URI.file(target.fsPath)); - assert.equal(existsSync(copied.resource.fsPath), true); - assert.equal(basename(copied.resource.fsPath), 'index.html'); + assert.strictEqual(existsSync(copied.resource.fsPath), true); + assert.strictEqual(basename(copied.resource.fsPath), 'index.html'); assert.ok(event!); - assert.equal(event!.resource.fsPath, source.resource.fsPath); - assert.equal(event!.operation, FileOperation.COPY); - assert.equal(event!.target!.resource.fsPath, copied.resource.fsPath); + assert.strictEqual(event!.resource.fsPath, source.resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.COPY); + assert.strictEqual(event!.target!.resource.fsPath, copied.resource.fsPath); copied = await service.resolve(source.resource, { resolveMetadata: true }); - assert.equal(source.size, copied.size); + assert.strictEqual(source.size, copied.size); }); test('readFile - small file - default', () => { @@ -1193,7 +1184,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ async function testReadFile(resource: URI): Promise { const content = await service.readFile(resource); - assert.equal(content.value.toString(), readFileSync(resource.fsPath)); + assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString()); } test('readFileStream - small file - default', () => { @@ -1221,7 +1212,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ async function testReadFileStream(resource: URI): Promise { const content = await service.readFileStream(resource); - assert.equal((await streamToBuffer(content.value)).toString(), readFileSync(resource.fsPath)); + assert.strictEqual((await streamToBuffer(content.value)).toString(), readFileSync(resource.fsPath).toString()); } test('readFile - Files are intermingled #38331 - default', async () => { @@ -1260,8 +1251,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ service.readFile(resource2) ]); - assert.equal(result[0].value.toString(), value1.value.toString()); - assert.equal(result[1].value.toString(), value2.value.toString()); + assert.strictEqual(result[0].value.toString(), value1.value.toString()); + assert.strictEqual(result[1].value.toString(), value2.value.toString()); } test('readFile - from position (ASCII) - default', async () => { @@ -1291,7 +1282,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const contents = await service.readFile(resource, { position: 6 }); - assert.equal(contents.value.toString(), 'File'); + assert.strictEqual(contents.value.toString(), 'File'); } test('readFile - from position (with umlaut) - default', async () => { @@ -1321,7 +1312,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const contents = await service.readFile(resource, { position: Buffer.from('Small File with Ü').length }); - assert.equal(contents.value.toString(), 'mlaut'); + assert.strictEqual(contents.value.toString(), 'mlaut'); } test('readFile - 3 bytes (ASCII) - default', async () => { @@ -1351,7 +1342,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const contents = await service.readFile(resource, { length: 3 }); - assert.equal(contents.value.toString(), 'Sma'); + assert.strictEqual(contents.value.toString(), 'Sma'); } test('readFile - 20000 bytes (large) - default', async () => { @@ -1403,7 +1394,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const contents = await service.readFile(resource, { length }); - assert.equal(contents.value.byteLength, length); + assert.strictEqual(contents.value.byteLength, length); } test('readFile - FILE_IS_DIRECTORY', async () => { @@ -1417,7 +1408,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); }); (isWindows /* error code does not seem to be supported on windows */ ? test.skip : test)('readFile - FILE_NOT_DIRECTORY', async () => { @@ -1431,7 +1422,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_DIRECTORY); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_DIRECTORY); }); test('readFile - FILE_NOT_FOUND', async () => { @@ -1445,7 +1436,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); }); test('readFile - FILE_NOT_MODIFIED_SINCE - default', async () => { @@ -1484,8 +1475,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); - assert.equal(fileProvider.totalBytesRead, 0); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); + assert.strictEqual(fileProvider.totalBytesRead, 0); } test('readFile - FILE_NOT_MODIFIED_SINCE does not fire wrongly - https://github.com/microsoft/vscode/issues/72909', async () => { @@ -1546,26 +1537,26 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT); } - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - default', async () => { + test('readFile - FILE_TOO_LARGE - default', async () => { return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - buffered', async () => { + test('readFile - FILE_TOO_LARGE - buffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - unbuffered', async () => { + test('readFile - FILE_TOO_LARGE - unbuffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); return testFileTooLarge(); }); - (isWindows ? test.skip /* flaky test */ : test)('readFile - FILE_TOO_LARGE - streamed', async () => { + test('readFile - FILE_TOO_LARGE - streamed', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream); return testFileTooLarge(); @@ -1590,7 +1581,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ } assert.ok(error); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE); } test('createFile', async () => { @@ -1612,16 +1603,16 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const contents = 'Hello World'; const resource = URI.file(join(testDir, 'test.txt')); - assert.equal(await service.canCreateFile(resource), true); + assert.strictEqual(await service.canCreateFile(resource), true); const fileStat = await service.createFile(resource, converter(contents)); - assert.equal(fileStat.name, 'test.txt'); - assert.equal(existsSync(fileStat.resource.fsPath), true); - assert.equal(readFileSync(fileStat.resource.fsPath), contents); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual(existsSync(fileStat.resource.fsPath), true); + assert.strictEqual(readFileSync(fileStat.resource.fsPath).toString(), contents); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, resource.fsPath); } test('createFile (does not overwrite by default)', async () => { @@ -1651,16 +1642,16 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ writeFileSync(resource.fsPath, ''); // create file - assert.equal(await service.canCreateFile(resource, { overwrite: true }), true); + assert.strictEqual(await service.canCreateFile(resource, { overwrite: true }), true); const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true }); - assert.equal(fileStat.name, 'test.txt'); - assert.equal(existsSync(fileStat.resource.fsPath), true); - assert.equal(readFileSync(fileStat.resource.fsPath), contents); + assert.strictEqual(fileStat.name, 'test.txt'); + assert.strictEqual(existsSync(fileStat.resource.fsPath), true); + assert.strictEqual(readFileSync(fileStat.resource.fsPath).toString(), contents); assert.ok(event!); - assert.equal(event!.resource.fsPath, resource.fsPath); - assert.equal(event!.operation, FileOperation.CREATE); - assert.equal(event!.target!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.resource.fsPath, resource.fsPath); + assert.strictEqual(event!.operation, FileOperation.CREATE); + assert.strictEqual(event!.target!.resource.fsPath, resource.fsPath); }); test('writeFile - default', async () => { @@ -1682,13 +1673,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ async function testWriteFile() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent)); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (large file) - default', async () => { @@ -1714,9 +1705,9 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const newContent = content.toString() + content.toString(); const fileStat = await service.writeFile(resource, VSBuffer.fromString(newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile - buffered - readonly throws', async () => { @@ -1734,8 +1725,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ async function testWriteFileReadonlyThrows() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; @@ -1757,7 +1748,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => { const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); })); const fileContent = readFileSync(resource.fsPath).toString(); @@ -1783,13 +1774,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ async function testWriteFileReadable() { const resource = URI.file(join(testDir, 'small.txt')); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, toLineByLineReadable(newContent)); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (large file - readable) - default', async () => { @@ -1815,9 +1806,9 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const newContent = content.toString() + content.toString(); const fileStat = await service.writeFile(resource, toLineByLineReadable(newContent)); - assert.equal(fileStat.name, 'lorem.txt'); + assert.strictEqual(fileStat.name, 'lorem.txt'); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); } test('writeFile (stream) - default', async () => { @@ -1841,10 +1832,10 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const target = URI.file(join(testDir, 'small-copy.txt')); const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); - assert.equal(fileStat.name, 'small-copy.txt'); + assert.strictEqual(fileStat.name, 'small-copy.txt'); const targetContents = readFileSync(target.fsPath).toString(); - assert.equal(readFileSync(source.fsPath).toString(), targetContents); + assert.strictEqual(readFileSync(source.fsPath).toString(), targetContents); } test('writeFile (large file - stream) - default', async () => { @@ -1868,10 +1859,10 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const target = URI.file(join(testDir, 'lorem-copy.txt')); const fileStat = await service.writeFile(target, streamToBufferReadableStream(createReadStream(source.fsPath))); - assert.equal(fileStat.name, 'lorem-copy.txt'); + assert.strictEqual(fileStat.name, 'lorem-copy.txt'); const targetContents = readFileSync(target.fsPath).toString(); - assert.equal(readFileSync(source.fsPath).toString(), targetContents); + assert.strictEqual(readFileSync(source.fsPath).toString(), targetContents); } test('writeFile (file is created including parents)', async () => { @@ -1879,9 +1870,9 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const content = 'File is created including parent'; const fileStat = await service.writeFile(resource, VSBuffer.fromString(content)); - assert.equal(fileStat.name, 'newfile.txt'); + assert.strictEqual(fileStat.name, 'newfile.txt'); - assert.equal(readFileSync(resource.fsPath), content); + assert.strictEqual(readFileSync(resource.fsPath).toString(), content); }); test('writeFile (error when folder is encountered)', async () => { @@ -1902,13 +1893,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const stat = await service.resolve(resource); - const content = readFileSync(resource.fsPath); - assert.equal(content, 'Small File'); + const content = readFileSync(resource.fsPath).toString(); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); - assert.equal(readFileSync(resource.fsPath), newContent); + assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); }); test('writeFile - error when writing to file that has been updated meanwhile', async () => { @@ -1917,7 +1908,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const stat = await service.resolve(resource); const content = readFileSync(resource.fsPath).toString(); - assert.equal(content, 'Small File'); + assert.strictEqual(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); @@ -1936,7 +1927,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ assert.ok(error); assert.ok(error instanceof FileOperationError); - assert.equal(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); + assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); }); test('writeFile - no error when writing to file where size is the same', async () => { @@ -1945,7 +1936,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const stat = await service.resolve(resource); const content = readFileSync(resource.fsPath).toString(); - assert.equal(content, 'Small File'); + assert.strictEqual(content, 'Small File'); const newContent = content; // same content await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); @@ -2008,73 +1999,72 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const runWatchTests = isLinux; - (runWatchTests ? test : test.skip)('watch - file', done => { + (runWatchTests ? test : test.skip)('watch - file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests && !isWindows /* symbolic links not reliable on windows */ ? test : test.skip)('watch - file symbolic link', async done => { + (runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => { const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); - await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); - - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + await promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - multiple writes', done => { + (runWatchTests ? test : test.skip)('watch - file - multiple writes', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 1'), 0); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 2'), 10); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 3'), 20); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - delete file', done => { + (runWatchTests ? test : test.skip)('watch - file - delete file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]); setTimeout(() => unlinkSync(toWatch.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - rename file', done => { + (runWatchTests ? test : test.skip)('watch - file - rename file', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); const toWatchRenamed = URI.file(join(testDir, 'index-watch1-renamed.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - + const promise = assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]); setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file - rename file (different case)', done => { + (runWatchTests ? test : test.skip)('watch - file - rename file (different case)', async () => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); const toWatchRenamed = URI.file(join(testDir, 'INDEX-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); - if (isLinux) { - assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done); - } else { - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); // case insensitive file system treat this as change - } + const promise = isLinux + ? assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]]) + : assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); // case insensitive file system treat this as change setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - file (atomic save)', function (done) { + (runWatchTests ? test : test.skip)('watch - file (atomic save)', async () => { const toWatch = URI.file(join(testDir, 'index-watch2.html')); writeFileSync(toWatch.fsPath, 'Init'); - assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); + const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => { // Simulate atomic save by deleting the file, creating it under different name @@ -2084,79 +2074,81 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ writeFileSync(renamed, 'Changes'); renameSync(renamed, toWatch.fsPath); }, 50); + + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - change file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - change file', async () => { const watchDir = URI.file(join(testDir, 'watch3')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.UPDATED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add file', async () => { const watchDir = URI.file(join(testDir, 'watch4')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); - assertWatch(watchDir, [[FileChangeType.ADDED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.ADDED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete file', async () => { const watchDir = URI.file(join(testDir, 'watch5')); mkdirSync(watchDir.fsPath); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.DELETED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file]]); setTimeout(() => unlinkSync(file.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add folder', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - add folder', async () => { const watchDir = URI.file(join(testDir, 'watch6')); mkdirSync(watchDir.fsPath); const folder = URI.file(join(watchDir.fsPath, 'folder')); - assertWatch(watchDir, [[FileChangeType.ADDED, folder]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.ADDED, folder]]); setTimeout(() => mkdirSync(folder.fsPath), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete folder', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - delete folder', async () => { const watchDir = URI.file(join(testDir, 'watch7')); mkdirSync(watchDir.fsPath); const folder = URI.file(join(watchDir.fsPath, 'folder')); mkdirSync(folder.fsPath); - assertWatch(watchDir, [[FileChangeType.DELETED, folder]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, folder]]); setTimeout(() => rimrafSync(folder.fsPath), 50); + await promise; }); - (runWatchTests && !isWindows /* symbolic links not reliable on windows */ ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await symlink(join(testDir, 'deep'), watchDir.fsPath); + await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); - assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.UPDATED, file]]); setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50); + await promise; }); - (runWatchTests ? test : test.skip)('watch - folder (non recursive) - rename file', done => { + (runWatchTests ? test : test.skip)('watch - folder (non recursive) - rename file', async () => { const watchDir = URI.file(join(testDir, 'watch8')); mkdirSync(watchDir.fsPath); @@ -2165,12 +2157,12 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const fileRenamed = URI.file(join(watchDir.fsPath, 'index-renamed.html')); - assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]]); setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + await promise; }); - (runWatchTests && isLinux /* this test requires a case sensitive file system */ ? test : test.skip)('watch - folder (non recursive) - rename file (different case)', done => { + (runWatchTests && isLinux /* this test requires a case sensitive file system */ ? test : test.skip)('watch - folder (non recursive) - rename file (different case)', async () => { const watchDir = URI.file(join(testDir, 'watch8')); mkdirSync(watchDir.fsPath); @@ -2179,46 +2171,48 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ const fileRenamed = URI.file(join(watchDir.fsPath, 'INDEX.html')); - assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done); - + const promise = assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]]); setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50); + await promise; }); - function assertWatch(toWatch: URI, expected: [FileChangeType, URI][], done: MochaDone): void { - const watcherDisposable = service.watch(toWatch); + function assertWatch(toWatch: URI, expected: [FileChangeType, URI][]): Promise { + return new Promise((resolve, reject) => { + const watcherDisposable = service.watch(toWatch); - function toString(type: FileChangeType): string { - switch (type) { - case FileChangeType.ADDED: return 'added'; - case FileChangeType.DELETED: return 'deleted'; - case FileChangeType.UPDATED: return 'updated'; - } - } - - function printEvents(event: FileChangesEvent): string { - return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); - } - - const listenerDisposable = service.onDidFilesChange(event => { - watcherDisposable.dispose(); - listenerDisposable.dispose(); - - try { - assert.equal(event.changes.length, expected.length, `Expected ${expected.length} events, but got ${event.changes.length}. Details (${printEvents(event)})`); - - if (expected.length === 1) { - assert.equal(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}. Details (${printEvents(event)})`); - assert.equal(event.changes[0].resource.fsPath, expected[0][1].fsPath); - } else { - for (const expect of expected) { - assert.equal(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}. Details (${printEvents(event)})`); - } + function toString(type: FileChangeType): string { + switch (type) { + case FileChangeType.ADDED: return 'added'; + case FileChangeType.DELETED: return 'deleted'; + case FileChangeType.UPDATED: return 'updated'; } - - done(); - } catch (error) { - done(error); } + + function printEvents(event: FileChangesEvent): string { + return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n'); + } + + const listenerDisposable = service.onDidFilesChange(event => { + watcherDisposable.dispose(); + listenerDisposable.dispose(); + + try { + assert.strictEqual(event.changes.length, expected.length, `Expected ${expected.length} events, but got ${event.changes.length}. Details (${printEvents(event)})`); + + if (expected.length === 1) { + assert.strictEqual(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}. Details (${printEvents(event)})`); + assert.strictEqual(event.changes[0].resource.fsPath, expected[0][1].fsPath); + } else { + for (const expect of expected) { + assert.strictEqual(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}. Details (${printEvents(event)})`); + } + } + + resolve(); + } catch (error) { + reject(error); + } + }); }); } @@ -2234,7 +2228,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ let fd = await fileProvider.open(resource, { create: false }); for (let i = 0; i < 3; i++) { await fileProvider.read(fd, 0, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); } await fileProvider.close(fd); @@ -2245,31 +2239,31 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ let posInFile = 0; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); posInFile += 26; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 1); - assert.equal(buffer.slice(0, 1).toString(), ','); + assert.strictEqual(buffer.slice(0, 1).toString(), ','); posInFile += 1; await fileProvider.read(fd, posInFile, buffer.buffer, 0, 12); - assert.equal(buffer.slice(0, 12).toString(), ' consectetur'); + assert.strictEqual(buffer.slice(0, 12).toString(), ' consectetur'); posInFile += 12; await fileProvider.read(fd, 98 /* no longer in sequence of posInFile */, buffer.buffer, 0, 9); - assert.equal(buffer.slice(0, 9).toString(), 'fermentum'); + assert.strictEqual(buffer.slice(0, 9).toString(), 'fermentum'); await fileProvider.read(fd, 27, buffer.buffer, 0, 12); - assert.equal(buffer.slice(0, 12).toString(), ' consectetur'); + assert.strictEqual(buffer.slice(0, 12).toString(), ' consectetur'); await fileProvider.read(fd, 26, buffer.buffer, 0, 1); - assert.equal(buffer.slice(0, 1).toString(), ','); + assert.strictEqual(buffer.slice(0, 1).toString(), ','); await fileProvider.read(fd, 0, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); await fileProvider.read(fd, posInFile /* back in sequence */, buffer.buffer, 0, 11); - assert.equal(buffer.slice(0, 11).toString(), ' adipiscing'); + assert.strictEqual(buffer.slice(0, 11).toString(), ' adipiscing'); await fileProvider.close(fd); }); @@ -2289,7 +2283,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ posInFileWrite += initialContents.byteLength; await fileProvider.read(fdRead, posInFileRead, buffer.buffer, 0, 26); - assert.equal(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); + assert.strictEqual(buffer.slice(0, 26).toString(), 'Lorem ipsum dolor sit amet'); posInFileRead += 26; const contents = VSBuffer.fromString('Hello World'); @@ -2298,19 +2292,19 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ posInFileWrite += contents.byteLength; await fileProvider.read(fdRead, posInFileRead, buffer.buffer, 0, contents.byteLength); - assert.equal(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); + assert.strictEqual(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); posInFileRead += contents.byteLength; await fileProvider.write(fdWrite, 6, contents.buffer, 0, contents.byteLength); await fileProvider.read(fdRead, 0, buffer.buffer, 0, 11); - assert.equal(buffer.slice(0, 11).toString(), 'Lorem Hello'); + assert.strictEqual(buffer.slice(0, 11).toString(), 'Lorem Hello'); await fileProvider.write(fdWrite, posInFileWrite, contents.buffer, 0, contents.byteLength); posInFileWrite += contents.byteLength; await fileProvider.read(fdRead, posInFileWrite - contents.byteLength, buffer.buffer, 0, contents.byteLength); - assert.equal(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); + assert.strictEqual(buffer.slice(0, contents.byteLength).toString(), 'Hello World'); await fileProvider.close(fdWrite); await fileProvider.close(fdRead); diff --git a/src/vs/platform/files/test/electron-browser/normalizer.test.ts b/src/vs/platform/files/test/electron-browser/normalizer.test.ts index e4f7477da9..65f8413f64 100644 --- a/src/vs/platform/files/test/electron-browser/normalizer.test.ts +++ b/src/vs/platform/files/test/electron-browser/normalizer.test.ts @@ -64,7 +64,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 3); + assert.strictEqual(e.changes.length, 3); assert.ok(e.contains(added, FileChangeType.ADDED)); assert.ok(e.contains(updated, FileChangeType.UPDATED)); assert.ok(e.contains(deleted, FileChangeType.DELETED)); @@ -103,7 +103,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 5); + assert.strictEqual(e.changes.length, 5); assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED)); assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED)); @@ -133,7 +133,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 1); + assert.strictEqual(e.changes.length, 1); assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); @@ -158,7 +158,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(deleted, FileChangeType.UPDATED)); assert.ok(e.contains(unrelated, FileChangeType.UPDATED)); @@ -184,7 +184,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(created, FileChangeType.ADDED)); assert.ok(!e.contains(created, FileChangeType.UPDATED)); @@ -213,7 +213,7 @@ suite('Normalizer', () => { watch.onDidFilesChange(e => { assert.ok(e); - assert.equal(e.changes.length, 2); + assert.strictEqual(e.changes.length, 2); assert.ok(e.contains(deleted, FileChangeType.DELETED)); assert.ok(!e.contains(updated, FileChangeType.UPDATED)); diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index b4f5f22e89..e582a434a6 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -132,15 +132,31 @@ export class InstantiationService implements IInstantiationService { private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T { let thing = this._getServiceInstanceOrDescriptor(id); if (thing instanceof SyncDescriptor) { - return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true)); + return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true)); } else { _trace.branch(id, false); return thing; } } + private readonly _activeInstantiations = new Set>(); + + + private _safeCreateAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T { + if (this._activeInstantiations.has(id)) { + throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`); + } + this._activeInstantiations.add(id); + try { + return this._createAndCacheServiceInstance(id, desc, _trace); + } finally { + this._activeInstantiations.delete(id); + } + } + private _createAndCacheServiceInstance(id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace): T { - type Triple = { id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace }; + + type Triple = { id: ServiceIdentifier, desc: SyncDescriptor, _trace: Trace; }; const graph = new Graph(data => data.id.toString()); let cycleCount = 0; @@ -195,7 +211,6 @@ export class InstantiationService implements IInstantiationService { graph.removeNode(data); } } - return this._getServiceInstanceOrDescriptor(id); } diff --git a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts index b1f5d1c7c8..c078c9472c 100644 --- a/src/vs/platform/ipc/electron-browser/sharedProcessService.ts +++ b/src/vs/platform/ipc/electron-browser/sharedProcessService.ts @@ -4,7 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; +import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp'; +import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Disposable } from 'vs/base/common/lifecycle'; export const ISharedProcessService = createDecorator('sharedProcessService'); @@ -14,7 +22,47 @@ export interface ISharedProcessService { getChannel(channelName: string): IChannel; registerChannel(channelName: string, channel: IServerChannel): void; - - whenSharedProcessReady(): Promise; - toggleSharedProcessWindow(): Promise; +} + +export class SharedProcessService extends Disposable implements ISharedProcessService { + + declare readonly _serviceBrand: undefined; + + private readonly withSharedProcessConnection: Promise; + + constructor( + @INativeHostService private readonly nativeHostService: INativeHostService, + @ILogService private readonly logService: ILogService + ) { + super(); + + this.withSharedProcessConnection = this.connect(); + } + + private async connect(): Promise { + this.logService.trace('Renderer->SharedProcess#connect'); + + // Ask to create message channel inside the window + // and send over a UUID to correlate the response + const nonce = generateUuid(); + ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce); + + // Wait until the main side has returned the `MessagePort` + // We need to filter by the `nonce` to ensure we listen + // to the right response. + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); + + this.logService.trace('Renderer->SharedProcess#connect: connection established'); + + return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`)); + } + + getChannel(channelName: string): IChannel { + return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName))); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel)); + } } diff --git a/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts b/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts deleted file mode 100644 index 4ac5810377..0000000000 --- a/src/vs/platform/ipc/electron-main/sharedProcessMainService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export const ISharedProcessMainService = createDecorator('sharedProcessMainService'); - -export interface ISharedProcessMainService { - - readonly _serviceBrand: undefined; - - whenSharedProcessReady(): Promise; - toggleSharedProcessWindow(): Promise; -} - -export interface ISharedProcess { - whenReady(): Promise; - toggle(): void; -} - -export class SharedProcessMainService implements ISharedProcessMainService { - - declare readonly _serviceBrand: undefined; - - constructor(private sharedProcess: ISharedProcess) { } - - whenSharedProcessReady(): Promise { - return this.sharedProcess.whenReady(); - } - - async toggleSharedProcessWindow(): Promise { - return this.sharedProcess.toggle(); - } -} diff --git a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts index a6f54d0afa..dd52714902 100644 --- a/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts +++ b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts @@ -3,10 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox'; +import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; export const IMainProcessService = createDecorator('mainProcessService'); @@ -19,18 +20,21 @@ export interface IMainProcessService { registerChannel(channelName: string, channel: IServerChannel): void; } -export class MainProcessService extends Disposable implements IMainProcessService { +/** + * An implementation of `IMainProcessService` that leverages Electron's IPC. + */ +export class ElectronIPCMainProcessService extends Disposable implements IMainProcessService { declare readonly _serviceBrand: undefined; - private mainProcessConnection: Client; + private mainProcessConnection: IPCElectronClient; constructor( windowId: number ) { super(); - this.mainProcessConnection = this._register(new Client(`window:${windowId}`)); + this.mainProcessConnection = this._register(new IPCElectronClient(`window:${windowId}`)); } getChannel(channelName: string): IChannel { @@ -41,3 +45,24 @@ export class MainProcessService extends Disposable implements IMainProcessServic this.mainProcessConnection.registerChannel(channelName, channel); } } + +/** + * An implementation of `IMainProcessService` that leverages MessagePorts. + */ +export class MessagePortMainProcessService implements IMainProcessService { + + declare readonly _serviceBrand: undefined; + + constructor( + private server: MessagePortServer, + private router: StaticRouter + ) { } + + getChannel(channelName: string): IChannel { + return this.server.getChannel(channelName, this.router); + } + + registerChannel(channelName: string, channel: IServerChannel): void { + this.server.registerChannel(channelName, channel); + } +} diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index fcbe69c6a7..93219b0041 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -56,6 +56,7 @@ export interface IssueReporterData extends WindowData { issueType?: IssueType; extensionId?: string; experiments?: string; + githubAccessToken: string; readonly issueTitle?: string; readonly issueBody?: string; } @@ -72,7 +73,6 @@ export interface IssueReporterFeatures { export interface ProcessExplorerStyles extends WindowStyles { hoverBackground?: string; hoverForeground?: string; - highlightForeground?: string; } export interface ProcessExplorerData extends WindowData { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 4319018383..870516ef38 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -17,7 +17,7 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; import { listProcesses } from 'vs/base/node/ps'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; import { FileAccess } from 'vs/base/common/network'; @@ -185,120 +185,118 @@ export class IssueMainService implements ICommonIssueService { } } - openReporter(data: IssueReporterData): Promise { - return new Promise(_ => { - if (!this._issueWindow) { - this._issueParentWindow = BrowserWindow.getFocusedWindow(); - if (this._issueParentWindow) { - const position = this.getWindowPosition(this._issueParentWindow, 700, 800); + async openReporter(data: IssueReporterData): Promise { + if (!this._issueWindow) { + this._issueParentWindow = BrowserWindow.getFocusedWindow(); + if (this._issueParentWindow) { + const position = this.getWindowPosition(this._issueParentWindow, 700, 800); - this._issueWindow = new BrowserWindow({ - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - title: localize('issueReporter', "Issue Reporter"), - backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + this._issueWindow = new BrowserWindow({ + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: localize('issueReporter', "Issue Reporter"), + backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: 'bypassHeatCheck', + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented + this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented - // Modified when testing UI - const features: IssueReporterFeatures = {}; + // Modified when testing UI + const features: IssueReporterFeatures = {}; - this.logService.trace('issueService#openReporter: opening issue reporter'); - this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); + this.logService.trace('issueService#openReporter: opening issue reporter'); + this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); - this._issueWindow.on('close', () => this._issueWindow = null); + this._issueWindow.on('close', () => this._issueWindow = null); - this._issueParentWindow.on('closed', () => { - if (this._issueWindow) { - this._issueWindow.close(); - this._issueWindow = null; - } - }); - } + this._issueParentWindow.on('closed', () => { + if (this._issueWindow) { + this._issueWindow.close(); + this._issueWindow = null; + } + }); } + } - if (this._issueWindow) { - this._issueWindow.focus(); - } - }); + if (this._issueWindow) { + this._issueWindow.focus(); + } } - openProcessExplorer(data: ProcessExplorerData): Promise { - return new Promise(_ => { - // Create as singleton - if (!this._processExplorerWindow) { - this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this._processExplorerParentWindow) { - const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); - this._processExplorerWindow = new BrowserWindow({ - skipTaskbar: true, - resizable: true, - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + async openProcessExplorer(data: ProcessExplorerData): Promise { + // Create as singleton + if (!this._processExplorerWindow) { + this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this._processExplorerParentWindow) { + const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); + this._processExplorerWindow = new BrowserWindow({ + skipTaskbar: true, + resizable: true, + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + v8CacheOptions: 'bypassHeatCheck', + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._processExplorerWindow.setMenuBarVisibility(false); + this._processExplorerWindow.setMenuBarVisibility(false); - const windowConfiguration = { - appRoot: this.environmentService.appRoot, - windowId: this._processExplorerWindow.id, - userEnv: this.userEnv, - machineId: this.machineId, - data - }; + const windowConfiguration = { + appRoot: this.environmentService.appRoot, + windowId: this._processExplorerWindow.id, + userEnv: this.userEnv, + machineId: this.machineId, + data + }; - this._processExplorerWindow.loadURL( - toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); + this._processExplorerWindow.loadURL( + toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); - this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); + this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); - this._processExplorerParentWindow.on('close', () => { - if (this._processExplorerWindow) { - this._processExplorerWindow.close(); - this._processExplorerWindow = null; - } - }); - } + this._processExplorerParentWindow.on('close', () => { + if (this._processExplorerWindow) { + this._processExplorerWindow.close(); + this._processExplorerWindow = null; + } + }); } + } - // Focus - if (this._processExplorerWindow) { - this._processExplorerWindow.focus(); - } - }); + // Focus + if (this._processExplorerWindow) { + this._processExplorerWindow.focus(); + } } public async getSystemStatus(): Promise { @@ -414,7 +412,7 @@ export class IssueMainService implements ICommonIssueService { }, product: { nameShort: product.nameShort, - version: product.version, + version: !!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version, commit: product.commit, date: product.date, reportIssueUrl: product.reportIssueUrl @@ -436,7 +434,7 @@ function toWindowUrl(modulePathToHtml: string, windowConfiguration: T): strin } return FileAccess - ._asCodeFileUri(modulePathToHtml, require) + .asBrowserUri(modulePathToHtml, require, true) .with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }) .toString(true); } diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 797c15e3bc..b33c75fcc3 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; -import { IntervalTimer } from 'vs/base/common/async'; +import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode, Keybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -35,6 +35,9 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChord: CurrentChord | null; private _currentChordChecker: IntervalTimer; private _currentChordStatusMessage: IDisposable | null; + private _currentSingleModifier: null | string; + private _currentSingleModifierClearTimeout: TimeoutTimer; + protected _logging: boolean; public get inChordMode(): boolean { @@ -53,6 +56,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK this._currentChord = null; this._currentChordChecker = new IntervalTimer(); this._currentChordStatusMessage = null; + this._currentSingleModifier = null; + this._currentSingleModifierClearTimeout = new TimeoutTimer(); this._logging = false; } @@ -166,30 +171,69 @@ export abstract class AbstractKeybindingService extends Disposable implements IK public dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void { const keybindings = this.resolveUserBinding(userSettingsLabel); if (keybindings.length >= 1) { - this._doDispatch(keybindings[0], target); + this._doDispatch(keybindings[0], target, /*isSingleModiferChord*/false); } } protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { - return this._doDispatch(this.resolveKeyboardEvent(e), target); + return this._doDispatch(this.resolveKeyboardEvent(e), target, /*isSingleModiferChord*/false); } - private _doDispatch(keybinding: ResolvedKeybinding, target: IContextKeyServiceTarget): boolean { + protected _singleModifierDispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean { + const keybinding = this.resolveKeyboardEvent(e); + const [singleModifier,] = keybinding.getSingleModifierDispatchParts(); + + if (singleModifier !== null && this._currentSingleModifier === null) { + // we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms + this._log(`+ Storing single modifier for possible chord ${singleModifier}.`); + this._currentSingleModifier = singleModifier; + this._currentSingleModifierClearTimeout.cancelAndSet(() => { + this._log(`+ Clearing single modifier due to 300ms elapsed.`); + this._currentSingleModifier = null; + }, 300); + return false; + } + + if (singleModifier !== null && singleModifier === this._currentSingleModifier) { + // bingo! + this._log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`); + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return this._doDispatch(keybinding, target, /*isSingleModiferChord*/true); + } + + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return false; + } + + private _doDispatch(keybinding: ResolvedKeybinding, target: IContextKeyServiceTarget, isSingleModiferChord = false): boolean { let shouldPreventDefault = false; if (keybinding.isChord()) { console.warn('Unexpected keyboard event mapped to a chord'); return false; } - const [firstPart,] = keybinding.getDispatchParts(); + + let firstPart: string | null = null; // the first keybinding i.e. Ctrl+K + let currentChord: string | null = null;// the "second" keybinding i.e. Ctrl+K "Ctrl+D" + + if (isSingleModiferChord) { + const [dispatchKeyname,] = keybinding.getSingleModifierDispatchParts(); + firstPart = dispatchKeyname; + currentChord = dispatchKeyname; + } else { + [firstPart,] = keybinding.getDispatchParts(); + currentChord = this._currentChord ? this._currentChord.keypress : null; + } + if (firstPart === null) { - this._log(`\\ Keyboard event cannot be dispatched.`); + this._log(`\\ Keyboard event cannot be dispatched in keydown phase.`); // cannot be dispatched, probably only modifier keys return shouldPreventDefault; } const contextValue = this._contextKeyService.getContext(target); - const currentChord = this._currentChord ? this._currentChord.keypress : null; const keypressLabel = keybinding.getLabel(); const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart); diff --git a/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts b/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts index acfe5d8d85..bccd632da5 100644 --- a/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts +++ b/src/vs/platform/keybinding/common/baseResolvedKeybinding.ts @@ -69,10 +69,15 @@ export abstract class BaseResolvedKeybinding extends Resolv return this._parts.map((keybinding) => this._getDispatchPart(keybinding)); } + public getSingleModifierDispatchParts(): (string | null)[] { + return this._parts.map((keybinding) => this._getSingleModifierDispatchPart(keybinding)); + } + protected abstract _getLabel(keybinding: T): string | null; protected abstract _getAriaLabel(keybinding: T): string | null; protected abstract _getElectronAccelerator(keybinding: T): string | null; protected abstract _getUserSettingsLabel(keybinding: T): string | null; protected abstract _isWYSIWYG(keybinding: T): boolean; protected abstract _getDispatchPart(keybinding: T): string | null; + protected abstract _getSingleModifierDispatchPart(keybinding: T): string | null; } diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 1e2e725a39..d1202ee543 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -3,9 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -340,39 +337,6 @@ export class KeybindingResolver { } return rules.evaluate(context); } - - public static getAllUnboundCommands(boundCommands: Map): string[] { - const unboundCommands: string[] = []; - const seenMap: Map = new Map(); - const addCommand = (id: string, includeCommandWithArgs: boolean) => { - if (seenMap.has(id)) { - return; - } - seenMap.set(id, true); - if (id[0] === '_' || id.indexOf('vscode.') === 0) { // private command - return; - } - if (boundCommands.get(id) === true) { - return; - } - if (!includeCommandWithArgs) { - const command = CommandsRegistry.getCommand(id); - if (command && typeof command.description === 'object' - && isNonEmptyArray((command.description).args)) { // command with args - return; - } - } - unboundCommands.push(id); - }; - for (const id of MenuRegistry.getCommands().keys()) { - addCommand(id, true); - } - for (const id of CommandsRegistry.getCommands().keys()) { - addCommand(id, false); - } - - return unboundCommands; - } } function printWhenExplanation(when: ContextKeyExpression | undefined): string { diff --git a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts index db213fd71d..b84879e991 100644 --- a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts +++ b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts @@ -23,6 +23,10 @@ export class ResolvedKeybindingItem { constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null, isBuiltinExtension: boolean) { this.resolvedKeybinding = resolvedKeybinding; this.keypressParts = resolvedKeybinding ? removeElementsAfterNulls(resolvedKeybinding.getDispatchParts()) : []; + if (resolvedKeybinding && this.keypressParts.length === 0) { + // handle possible single modifier chord keybindings + this.keypressParts = removeElementsAfterNulls(resolvedKeybinding.getSingleModifierDispatchParts()); + } this.bubble = (command ? command.charCodeAt(0) === CharCode.Caret : false); this.command = this.bubble ? command!.substr(1) : command; this.commandArgs = commandArgs; diff --git a/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts b/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts index 283c0676f1..1300b04431 100644 --- a/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts +++ b/src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts @@ -111,4 +111,20 @@ export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding { // send Ctrl/Cmd + K let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -225,13 +225,13 @@ suite('AbstractKeybindingService', () => { // send backspace shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.` ]); - assert.deepEqual(statusMessageCallsDisposed, [ + assert.deepStrictEqual(statusMessageCallsDisposed, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; @@ -241,14 +241,14 @@ suite('AbstractKeybindingService', () => { // send backspace shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -273,11 +273,11 @@ suite('AbstractKeybindingService', () => { function assertIsIgnored(keybinding: number): void { let shouldPreventDefault = kbService.testDispatch(keybinding); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -310,14 +310,14 @@ suite('AbstractKeybindingService', () => { key1: true }); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -326,13 +326,13 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, [ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -341,14 +341,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + X currentContextValue = createContext({}); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'chordCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, [ + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, [ `(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...` ]); executeCommandCalls = []; @@ -370,14 +370,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -388,14 +388,14 @@ suite('AbstractKeybindingService', () => { key1: true }); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, true); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, true); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -406,11 +406,11 @@ suite('AbstractKeybindingService', () => { key1: true }); shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, []); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; @@ -428,14 +428,14 @@ suite('AbstractKeybindingService', () => { // send Ctrl/Cmd + K currentContextValue = createContext({}); let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K); - assert.equal(shouldPreventDefault, false); - assert.deepEqual(executeCommandCalls, [{ + assert.strictEqual(shouldPreventDefault, false); + assert.deepStrictEqual(executeCommandCalls, [{ commandId: 'simpleCommand', args: [null] }]); - assert.deepEqual(showMessageCalls, []); - assert.deepEqual(statusMessageCalls, []); - assert.deepEqual(statusMessageCallsDisposed, []); + assert.deepStrictEqual(showMessageCalls, []); + assert.deepStrictEqual(statusMessageCalls, []); + assert.deepStrictEqual(statusMessageCallsDisposed, []); executeCommandCalls = []; showMessageCalls = []; statusMessageCalls = []; diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index 2b6b260a0f..f4e16e6e94 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -11,7 +11,7 @@ suite('KeybindingLabels', () => { function assertUSLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getLabel(), expected); } test('Windows US label', () => { @@ -116,7 +116,7 @@ suite('KeybindingLabels', () => { test('Aria label', () => { function assertAriaLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getAriaLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getAriaLabel(), expected); } assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A'); @@ -127,7 +127,7 @@ suite('KeybindingLabels', () => { test('Electron Accelerator label', () => { function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string | null): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getElectronAccelerator(), expected); + assert.strictEqual(usResolvedKeybinding.getElectronAccelerator(), expected); } assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A'); @@ -154,7 +154,7 @@ suite('KeybindingLabels', () => { test('User Settings label', () => { function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string): void { const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - assert.equal(usResolvedKeybinding.getUserSettingsLabel(), expected); + assert.strictEqual(usResolvedKeybinding.getUserSettingsLabel(), expected); } assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+win+a'); diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 5aec98875e..bac0d5f518 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -43,12 +43,12 @@ suite('KeybindingResolver', () => { let contextRules = ContextKeyExpr.equals('bar', 'baz'); let keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true); - assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); - assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); + assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); + assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); let resolver = new KeybindingResolver([keybindingItem], [], () => { }); - assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); - assert.equal(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); + assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); + assert.strictEqual(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); }); test('resolve key with arguments', function () { @@ -59,7 +59,7 @@ suite('KeybindingResolver', () => { let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true); let resolver = new KeybindingResolver([keybindingItem], [], () => { }); - assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); + assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); }); test('KeybindingResolver.combine simple 1', function () { @@ -70,7 +70,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false), ]); @@ -85,7 +85,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true), kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false), @@ -101,7 +101,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); @@ -116,7 +116,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_B, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true), kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); @@ -131,7 +131,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -145,7 +145,7 @@ suite('KeybindingResolver', () => { kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -159,7 +159,7 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -173,7 +173,7 @@ suite('KeybindingResolver', () => { kbItem(0, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); @@ -187,17 +187,17 @@ suite('KeybindingResolver', () => { kbItem(KeyCode.KEY_A, '-yes1', null, null!, false) ]; let actual = KeybindingResolver.combine(defaults, overrides); - assert.deepEqual(actual, [ + assert.deepStrictEqual(actual, [ kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true) ]); }); test('contextIsEntirelyIncluded', () => { const assertIsIncluded = (a: string | null, b: string | null) => { - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); }; const assertIsNotIncluded = (a: string | null, b: string | null) => { - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); + assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); }; assertIsIncluded('key1', null); @@ -314,11 +314,11 @@ suite('KeybindingResolver', () => { let testKey = (commandId: string, expectedKeys: number[]) => { // Test lookup let lookupResult = resolver.lookupKeybindings(commandId); - assert.equal(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); + assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t')); for (let i = 0, len = lookupResult.length; i < len; i++) { const expected = new USLayoutResolvedKeybinding(createKeybinding(expectedKeys[i], OS)!, OS); - assert.equal(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId); + assert.strictEqual(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId); } }; @@ -333,14 +333,14 @@ suite('KeybindingResolver', () => { // if it's the final part, then we should find a valid command, // and there should not be a chord. assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`); } else { // if it's not the final part, then we should not find a valid command, // and there should be a chord. assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`); - assert.equal(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`); + assert.strictEqual(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`); } previousPart = part; } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index aa4c588756..a0f1c71423 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { IWorkspace } from 'vs/platform/workspace/common/workspace'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export const ILabelService = createDecorator('labelService'); @@ -23,7 +23,7 @@ export interface ILabelService { */ getUriLabel(resource: URI, options?: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean }): string; getUriBasenameLabel(resource: URI): string; - getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; + getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(scheme: string, authority?: string): string; getSeparator(scheme: string, authority?: string): '/' | '\\'; diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 04cab578bc..a906de98c1 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -9,10 +9,9 @@ import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron'; @@ -21,6 +20,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemote import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/node/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'; export const ID = 'launchMainService'; export const ILaunchMainService = createDecorator(ID); @@ -35,23 +35,6 @@ export interface IRemoteDiagnosticOptions { includeWorkspaceMetadata?: boolean; } -function parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] { - if (args['open-url'] && args._urls && args._urls.length > 0) { - // --open-url must contain -- followed by the url(s) - // process.argv is used over args._ as args._ are resolved to file paths at this point - return coalesce(args._urls - .map(url => { - try { - return { uri: URI.parse(url), url }; - } catch (err) { - return null; - } - })); - } - - return []; -} - export interface ILaunchMainService { readonly _serviceBrand: undefined; start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise; @@ -68,7 +51,7 @@ export class LaunchMainService implements ILaunchMainService { @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IURLService private readonly urlService: IURLService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IConfigurationService private readonly configurationService: IConfigurationService ) { } @@ -89,7 +72,7 @@ export class LaunchMainService implements ILaunchMainService { } // Check early for open-url which is handled in URL service - const urlsToOpen = parseOpenUrl(args); + const urlsToOpen = this.parseOpenUrl(args); if (urlsToOpen.length) { let whenWindowReady: Promise = Promise.resolve(); @@ -113,7 +96,24 @@ export class LaunchMainService implements ILaunchMainService { } } - private startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { + private parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] { + if (args['open-url'] && args._urls && args._urls.length > 0) { + // --open-url must contain -- followed by the url(s) + // process.argv is used over args._ as args._ are resolved to file paths at this point + return coalesce(args._urls + .map(url => { + try { + return { uri: URI.parse(url), url }; + } catch (err) { + return null; + } + })); + } + + return []; + } + + private async startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP; let usedWindows: ICodeWindow[] = []; @@ -213,17 +213,15 @@ export class LaunchMainService implements ILaunchMainService { whenDeleted(waitMarkerFileURI.fsPath) ]).then(() => undefined, () => undefined); } - - return Promise.resolve(undefined); } - getMainProcessId(): Promise { + async getMainProcessId(): Promise { this.logService.trace('Received request for process ID from other instance.'); - return Promise.resolve(process.pid); + return process.pid; } - getMainProcessInfo(): Promise { + async getMainProcessInfo(): Promise { this.logService.trace('Received request for main process info from other instance.'); const windows: IWindowInfo[] = []; @@ -236,18 +234,18 @@ export class LaunchMainService implements ILaunchMainService { } }); - return Promise.resolve({ + return { mainPID: process.pid, mainArguments: process.argv.slice(1), windows, screenReader: !!app.accessibilitySupportEnabled, gpuFeatureStatus: app.getGPUFeatureStatus() - }); + }; } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { + async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); - const promises: Promise[] = windows.map(window => { + const diagnostics: Array = await Promise.all(windows.map(window => { return new Promise((resolve) => { const remoteAuthority = window.remoteAuthority; if (remoteAuthority) { @@ -275,27 +273,26 @@ export class LaunchMainService implements ILaunchMainService { resolve(undefined); } }); - }); + })); - return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x)); + return diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x); } private getFolderURIs(window: ICodeWindow): URI[] { const folderURIs: URI[] = []; - if (window.openedFolderUri) { - folderURIs.push(window.openedFolderUri); - } else if (window.openedWorkspace) { - // workspace folders can only be shown for local workspaces - const workspaceConfigPath = window.openedWorkspace.configPath; - const resolvedWorkspace = this.workspacesMainService.resolveLocalWorkspaceSync(workspaceConfigPath); + const workspace = window.openedWorkspace; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + folderURIs.push(workspace.uri); + } else if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces if (resolvedWorkspace) { const rootFolders = resolvedWorkspace.folders; rootFolders.forEach(root => { folderURIs.push(root.uri); }); } else { - //TODO: can we add the workspace file here? + //TODO@RMacfarlane: can we add the workspace file here? } } @@ -304,13 +301,14 @@ export class LaunchMainService implements ILaunchMainService { private codeWindowToInfo(window: ICodeWindow): IWindowInfo { const folderURIs = this.getFolderURIs(window); + return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority); } - private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { + private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo { return { - pid: win.webContents.getOSProcessId(), - title: win.getTitle(), + pid: window.webContents.getOSProcessId(), + title: window.getTitle(), folderURIs, remoteAuthority }; diff --git a/src/vs/platform/lifecycle/common/lifecycle.ts b/src/vs/platform/lifecycle/common/lifecycle.ts index 64c7e4a3ff..41a27d2ad9 100644 --- a/src/vs/platform/lifecycle/common/lifecycle.ts +++ b/src/vs/platform/lifecycle/common/lifecycle.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isThenable } from 'vs/base/common/async'; +import { Promises, isThenable } from 'vs/base/common/async'; // Shared veto handling across main and renderer export function handleVetos(vetos: (boolean | Promise)[], onError: (error: Error) => void): Promise { @@ -33,5 +33,5 @@ export function handleVetos(vetos: (boolean | Promise)[], onError: (err } } - return Promise.all(promises).then(() => lazyValue); + return Promises.settled(promises).then(() => lazyValue); } diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index efd5e3c9df..12c2e04a79 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -12,7 +12,7 @@ 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 { Barrier, timeout } from 'vs/base/common/async'; +import { Promises, Barrier, timeout } from 'vs/base/common/async'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; export const ILifecycleMainService = createDecorator('lifecycleMainService'); @@ -274,7 +274,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } }); - this.pendingWillShutdownPromise = Promise.all(joiners).then(() => undefined, err => this.logService.error(err)); + this.pendingWillShutdownPromise = Promises.settled(joiners).then(() => undefined, err => this.logService.error(err)); return this.pendingWillShutdownPromise; } @@ -576,7 +576,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe if (window && !window.isDestroyed()) { let whenWindowClosed: Promise; if (window.webContents && !window.webContents.isDestroyed()) { - whenWindowClosed = new Promise(c => window.once('closed', c)); + whenWindowClosed = new Promise(resolve => window.once('closed', resolve)); } else { whenWindowClosed = Promise.resolve(); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 33aa355c77..e2c1a8cb5c 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -126,7 +126,7 @@ const automaticKeyboardNavigationSettingKey = 'workbench.list.automaticKeyboardN const treeIndentKey = 'workbench.tree.indent'; const treeRenderIndentGuidesKey = 'workbench.tree.renderIndentGuides'; const listSmoothScrolling = 'workbench.list.smoothScrolling'; -const treeExpandOnFolderClick = 'workbench.tree.expandOnFolderClick'; +const treeExpandMode = 'workbench.tree.expandMode'; function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { return configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; @@ -429,12 +429,14 @@ export interface IResourceNavigatorOptions { export interface SelectionKeyboardEvent extends KeyboardEvent { preserveFocus?: boolean; + pinned?: boolean; __forceEvent?: boolean; } -export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean): SelectionKeyboardEvent { +export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean, pinned?: boolean): SelectionKeyboardEvent { const e = new KeyboardEvent(typeArg); (e).preserveFocus = preserveFocus; + (e).pinned = pinned; (e).__forceEvent = true; return e; @@ -445,11 +447,11 @@ abstract class ResourceNavigator extends Disposable { private readonly openOnFocus: boolean; private openOnSingleClick: boolean; - private readonly _onDidOpen = this._register(new Emitter>()); - readonly onDidOpen: Event> = this._onDidOpen.event; + private readonly _onDidOpen = this._register(new Emitter>()); + readonly onDidOpen: Event> = this._onDidOpen.event; constructor( - private readonly widget: ListWidget, + protected readonly widget: ListWidget, options?: IResourceNavigatorOptions ) { super(); @@ -457,8 +459,8 @@ abstract class ResourceNavigator extends Disposable { this.openOnFocus = options?.openOnFocus ?? false; this._register(Event.filter(this.widget.onDidChangeSelection, e => e.browserEvent instanceof KeyboardEvent)(e => this.onSelectionFromKeyboard(e))); - this._register(this.widget.onPointer((e: { browserEvent: MouseEvent }) => this.onPointer(e.browserEvent))); - this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent }) => this.onMouseDblClick(e.browserEvent))); + this._register(this.widget.onPointer((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onPointer(e.element, e.browserEvent))); + this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onMouseDblClick(e.element, e.browserEvent))); if (this.openOnFocus) { this._register(Event.filter(this.widget.onDidChangeFocus, e => e.browserEvent instanceof KeyboardEvent)(e => this.onFocusFromKeyboard(e))); @@ -478,11 +480,12 @@ abstract class ResourceNavigator extends Disposable { const focus = this.widget.getFocus(); this.widget.setSelection(focus, event.browserEvent); - const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; - const pinned = !preserveFocus; + const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent; + const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true; + const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus; const sideBySide = false; - this._open(preserveFocus, pinned, sideBySide, event.browserEvent); + this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); } private onSelectionFromKeyboard(event: ITreeEvent): void { @@ -490,14 +493,15 @@ abstract class ResourceNavigator extends Disposable { return; } - const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; - const pinned = !preserveFocus; + const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent; + const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true; + const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus; const sideBySide = false; - this._open(preserveFocus, pinned, sideBySide, event.browserEvent); + this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); } - private onPointer(browserEvent: MouseEvent): void { + private onPointer(element: T | undefined, browserEvent: MouseEvent): void { if (!this.openOnSingleClick) { return; } @@ -513,10 +517,10 @@ abstract class ResourceNavigator extends Disposable { const pinned = isMiddleClick; const sideBySide = browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey; - this._open(preserveFocus, pinned, sideBySide, browserEvent); + this._open(element, preserveFocus, pinned, sideBySide, browserEvent); } - private onMouseDblClick(browserEvent?: MouseEvent): void { + private onMouseDblClick(element: T | undefined, browserEvent?: MouseEvent): void { if (!browserEvent) { return; } @@ -525,10 +529,14 @@ abstract class ResourceNavigator extends Disposable { const pinned = true; const sideBySide = (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey); - this._open(preserveFocus, pinned, sideBySide, browserEvent); + this._open(element, preserveFocus, pinned, sideBySide, browserEvent); } - private _open(preserveFocus: boolean, pinned: boolean, sideBySide: boolean, browserEvent?: UIEvent): void { + private _open(element: T | undefined, preserveFocus: boolean, pinned: boolean, sideBySide: boolean, browserEvent?: UIEvent): void { + if (!element) { + return; + } + this._onDidOpen.fire({ editorOptions: { preserveFocus, @@ -536,27 +544,39 @@ abstract class ResourceNavigator extends Disposable { revealIfVisible: true }, sideBySide, - element: this.widget.getSelection()[0], + element, browserEvent }); } + + abstract getSelectedElement(): T | undefined; } -export class ListResourceNavigator extends ResourceNavigator { +export class ListResourceNavigator extends ResourceNavigator { + constructor( - list: List | PagedList, + protected readonly widget: List | PagedList, options?: IResourceNavigatorOptions ) { - super(list, options); + super(widget, options); + } + + getSelectedElement(): T | undefined { + return this.widget.getSelectedElements()[0]; } } class TreeResourceNavigator extends ResourceNavigator { + constructor( - tree: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree, + protected readonly widget: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree, options: IResourceNavigatorOptions ) { - super(tree, options); + super(widget, options); + } + + getSelectedElement(): T | undefined { + return this.widget.getSelection()[0] ?? undefined; } } @@ -591,7 +611,7 @@ export class WorkbenchObjectTree, TFilterData = void> private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -627,7 +647,7 @@ export class WorkbenchCompressibleObjectTree, TFilter private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -671,7 +691,7 @@ export class WorkbenchDataTree extends DataTree; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -716,7 +736,7 @@ export class WorkbenchAsyncDataTree extends Async private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -758,7 +778,7 @@ export class WorkbenchCompressibleAsyncDataTree e private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -810,7 +830,7 @@ function workbenchTreeDataPreamble(keyboardNavigationSettingKey); + const keyboardNavigation = options.simpleKeyboardNavigation || accessibilityOn ? 'simple' : configurationService.getValue(keyboardNavigationSettingKey); const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue(horizontalScrollingKey); const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); const additionalScrollHeight = options.additionalScrollHeight; @@ -833,7 +853,7 @@ function workbenchTreeDataPreamble(treeExpandOnFolderClick) + expandOnlyOnTwistieClick: options.expandOnlyOnTwistieClick ?? (configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick') } as TOptions }; } @@ -849,7 +869,7 @@ class WorkbenchTreeInternals { private styler: IDisposable | undefined; private navigator: TreeResourceNavigator; - get onDidOpen(): Event> { return this.navigator.onDidOpen; } + get onDidOpen(): Event> { return this.navigator.onDidOpen; } constructor( private tree: WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, @@ -935,8 +955,8 @@ class WorkbenchTreeInternals { if (e.affectsConfiguration(openModeSettingKey)) { newOptions = { ...newOptions, expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick' }; } - if (e.affectsConfiguration(treeExpandOnFolderClick) && options.expandOnlyOnTwistieClick === undefined) { - newOptions = { ...newOptions, expandOnlyOnTwistieClick: !configurationService.getValue(treeExpandOnFolderClick) }; + if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) { + newOptions = { ...newOptions, expandOnlyOnTwistieClick: configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick' }; } if (Object.keys(newOptions).length > 0) { tree.updateOptions(newOptions); @@ -1042,10 +1062,11 @@ configurationRegistry.registerConfiguration({ 'default': true, markdownDescription: localize('automatic keyboard navigation setting', "Controls whether keyboard navigation in lists and trees is automatically triggered simply by typing. If set to `false`, keyboard navigation is only triggered when executing the `list.toggleKeyboardNavigation` command, for which you can assign a keyboard shortcut.") }, - [treeExpandOnFolderClick]: { - type: 'boolean', - default: true, - description: localize('list expand on folder click setting', "Controls whether tree folders are expanded when clicking the folder names."), + [treeExpandMode]: { + type: 'string', + enum: ['singleClick', 'doubleClick'], + default: 'singleClick', + description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names."), } } }); diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts index ca701d7ecb..f3de69e182 100644 --- a/src/vs/platform/localizations/common/localizations.ts +++ b/src/vs/platform/localizations/common/localizations.ts @@ -25,6 +25,8 @@ export interface ILocalizationsService { readonly onDidLanguagesChange: Event; getLanguageIds(): Promise; + + update(): Promise; } export function isValidLocalization(localization: ILocalization): boolean { diff --git a/src/vs/platform/localizations/node/localizations.ts b/src/vs/platform/localizations/node/localizations.ts index 87d72b1874..c541b256c4 100644 --- a/src/vs/platform/localizations/node/localizations.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as pfs from 'vs/base/node/pfs'; +import { writeFile } from 'vs/base/node/pfs'; +import { promises } from 'fs'; import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -73,10 +74,10 @@ export class LocalizationsService extends Disposable implements ILocalizationsSe }); } - update(): Promise { - return Promise.all([this.cache.getLanguagePacks(), this.extensionManagementService.getInstalled()]) - .then(([current, installed]) => this.cache.update(installed) - .then(updated => !equals(Object.keys(current), Object.keys(updated)))); + async update(): Promise { + const [current, installed] = await Promise.all([this.cache.getLanguagePacks(), this.extensionManagementService.getInstalled()]); + const updated = await this.cache.update(installed); + return !equals(Object.keys(current), Object.keys(updated)); } } @@ -157,7 +158,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return pfs.readFile(this.languagePacksFilePath, 'utf8') + return promises.readFile(this.languagePacksFilePath, 'utf8') .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) @@ -171,7 +172,7 @@ class LanguagePacksCache extends Disposable { this.initializedCache = true; const raw = JSON.stringify(this.languagePacks); this.logService.debug('Writing language packs', raw); - return pfs.writeFile(this.languagePacksFilePath, raw); + return writeFile(this.languagePacksFilePath, raw); }) .then(() => result, error => this.logService.error(error)); }); diff --git a/src/vs/platform/log/node/loggerService.ts b/src/vs/platform/log/node/loggerService.ts index e465db7471..7456a1a20e 100644 --- a/src/vs/platform/log/node/loggerService.ts +++ b/src/vs/platform/log/node/loggerService.ts @@ -9,8 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { basename, extname, dirname } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import { IFileService } from 'vs/platform/files/common/files'; export class LoggerService extends Disposable implements ILoggerService { @@ -20,7 +20,7 @@ export class LoggerService extends Disposable implements ILoggerService { constructor( @ILogService private logService: ILogService, - @IInstantiationService private instantiationService: IInstantiationService, + @IFileService private fileService: IFileService ) { super(); this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level)))); @@ -34,7 +34,7 @@ export class LoggerService extends Disposable implements ILoggerService { const ext = extname(resource); logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).fsPath, this.logService.getLevel()); } else { - logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel()); + logger = new FileLogService(basename(resource), resource, this.logService.getLevel(), this.fileService); } this.loggers.set(resource.toString(), logger); } diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 5af7b1170f..3e5c5b0294 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -145,14 +145,11 @@ export class MarkerService implements IMarkerService { readonly onMarkerChanged: Event = Event.debounce(this._onMarkerChanged.event, MarkerService._debouncer, 0); private readonly _data = new DoubleResourceMap(); - private readonly _stats: MarkerStats; - - constructor() { - this._stats = new MarkerStats(this); - } + private readonly _stats = new MarkerStats(this); dispose(): void { this._stats.dispose(); + this._onMarkerChanged.dispose(); } getStatistics(): MarkerStatistics { diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index e1d67a917b..6903b26884 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -6,9 +6,8 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron'; +import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, KeyboardEvent } from 'electron'; import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -16,7 +15,7 @@ import product from 'vs/platform/product/common/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { mnemonicMenuLabel } from 'vs/base/common/labels'; -import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; @@ -62,7 +61,7 @@ export class Menubar { private keybindings: { [commandId: string]: IMenubarKeybinding }; - private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: Event) => void } = Object.create(null); + private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: KeyboardEvent) => void } = Object.create(null); constructor( @IUpdateService private readonly updateService: IUpdateService, @@ -609,45 +608,18 @@ export class Menubar { } } - private static _menuItemIsTriggeredViaKeybinding(event: KeyboardEvent, userSettingsLabel: string): boolean { - // The event coming in from Electron will inform us only about the modifier keys pressed. - // The strategy here is to check if the modifier keys match those of the keybinding, - // since it is highly unlikely to use modifier keys when clicking with the mouse - if (!userSettingsLabel) { - // There is no keybinding - return false; - } - - let ctrlRequired = /ctrl/.test(userSettingsLabel); - let shiftRequired = /shift/.test(userSettingsLabel); - let altRequired = /alt/.test(userSettingsLabel); - let metaRequired = /cmd/.test(userSettingsLabel) || /super/.test(userSettingsLabel); - - if (!ctrlRequired && !shiftRequired && !altRequired && !metaRequired) { - // This keybinding does not use any modifier keys, so we cannot use this heuristic - return false; - } - - return ( - ctrlRequired === event.ctrlKey - && shiftRequired === event.shiftKey - && altRequired === event.altKey - && metaRequired === event.metaKey - ); - } - private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): MenuItem { const label = this.mnemonicLabel(arg1); - const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: Event) => { + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: KeyboardEvent) => { const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null; let commandId = arg2; if (Array.isArray(arg2)) { commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking } - if (userSettingsLabel && Menubar._menuItemIsTriggeredViaKeybinding(event, userSettingsLabel)) { + if (userSettingsLabel && event.triggeredByAccelerator) { this.runActionInRenderer({ type: 'keybinding', userSettingsLabel }); } else { this.runActionInRenderer({ type: 'commandId', commandId }); @@ -707,8 +679,8 @@ export class Menubar { return new MenuItem(this.withKeybinding(commandId, options)); } - private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void { - return () => { + private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BrowserWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => void { + return (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => { // No Active Window const activeWindow = BrowserWindow.getFocusedWindow(); @@ -723,7 +695,7 @@ export class Menubar { } // Finally execute command in Window - click(); + click(menuItem, win || activeWindow, event); }; } diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 1f5773ceeb..1ca1df19fa 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -49,7 +49,7 @@ export interface ICommonNativeHostService { readonly onDidChangeColorScheme: Event; - readonly onDidChangePassword: Event; + readonly onDidChangePassword: Event<{ service: string, account: string }>; // Window getWindows(): Promise; @@ -138,6 +138,7 @@ export interface ICommonNativeHostService { // Development openDevTools(options?: OpenDevToolsOptions): Promise; toggleDevTools(): Promise; + toggleSharedProcessWindow(): Promise; sendInputEvent(event: MouseInputEvent): Promise; // Connectivity diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 79b2d02f6f..4de2693a1b 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; -import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; -import { dirExists } from 'vs/base/node/pfs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { SymlinkSupport } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -28,6 +27,7 @@ import { dirname, join } from 'vs/base/common/path'; import product from 'vs/platform/product/common/product'; import { memoize } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -43,6 +43,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain declare readonly _serviceBrand: undefined; constructor( + private sharedProcess: ISharedProcess, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -91,7 +92,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain private readonly _onDidChangeColorScheme = this._register(new Emitter()); readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event; - private readonly _onDidChangePassword = this._register(new Emitter()); + private readonly _onDidChangePassword = this._register(new Emitter<{ account: string, service: string }>()); readonly onDidChangePassword = this._onDidChangePassword.event; //#endregion @@ -104,7 +105,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return windows.map(window => ({ id: window.id, workspace: window.openedWorkspace, - folderUri: window.openedFolderUri, title: window.win.getTitle(), filename: window.getRepresentedFilename(), dirty: window.isDocumentEdited() @@ -261,7 +261,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const paths = await this.dialogMainService.pickFileFolder(options); if (paths) { this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData); - this.doOpenPicked(await Promise.all(paths.map(async path => (await dirExists(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); + this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.existsDirectory(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); } } @@ -334,8 +334,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async openExternal(windowId: number | undefined, url: string): Promise { - if (isLinux && process.env.SNAP && process.env.SNAP_REVISION) { - NativeHostMainService._safeSnapOpenExternal(url); + if (isLinuxSnap) { + this.safeSnapOpenExternal(url); } else { shell.openExternal(url); } @@ -343,7 +343,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return true; } - private static _safeSnapOpenExternal(url: string): void { + private safeSnapOpenExternal(url: string): void { + + // Remove some environment variables before opening to avoid issues... const gdkPixbufModuleFile = process.env['GDK_PIXBUF_MODULE_FILE']; const gdkPixbufModuleDir = process.env['GDK_PIXBUF_MODULEDIR']; delete process.env['GDK_PIXBUF_MODULE_FILE']; @@ -351,6 +353,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain shell.openExternal(url); + // ...but restore them after process.env['GDK_PIXBUF_MODULE_FILE'] = gdkPixbufModuleFile; process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir; } @@ -631,6 +634,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async toggleSharedProcessWindow(): Promise { + return this.sharedProcess.toggle(); + } + //#endregion //#region Registry (windows) @@ -708,7 +715,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain await keytar.setPassword(service, account, password); } - this._onDidChangePassword.fire(); + this._onDidChangePassword.fire({ service, account }); } async deletePassword(windowId: number | undefined, service: string, account: string): Promise { @@ -716,7 +723,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const didDelete = await keytar.deletePassword(service, account); if (didDelete) { - this._onDidChangePassword.fire(); + this._onDidChangePassword.fire({ service, account }); } return didDelete; diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index a3a144c1ed..957c8ce629 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -20,11 +20,13 @@ export interface ILinkDescriptor { export interface ILinkStyles { readonly textLinkForeground?: Color; + readonly disabled?: boolean; } export class Link extends Disposable { readonly el: HTMLAnchorElement; + private disabled: boolean; private styles: ILinkStyles = { textLinkForeground: Color.fromHex('#006AB1') }; @@ -53,6 +55,7 @@ export class Link extends Disposable { openerService.open(link.href, { allowCommands: true }); })); + this.disabled = false; this.applyStyles(); } @@ -62,6 +65,26 @@ export class Link extends Disposable { } private applyStyles(): void { - this.el.style.color = this.styles.textLinkForeground?.toString() || ''; + const color = this.styles.textLinkForeground?.toString(); + if (color) { + this.el.style.color = color; + } + if (typeof this.styles.disabled === 'boolean' && this.styles.disabled !== this.disabled) { + if (this.styles.disabled) { + this.el.setAttribute('aria-disabled', 'true'); + this.el.tabIndex = -1; + this.el.style.pointerEvents = 'none'; + this.el.style.opacity = '0.4'; + this.el.style.cursor = 'default'; + this.disabled = true; + } else { + this.el.setAttribute('aria-disabled', 'false'); + this.el.tabIndex = 0; + this.el.style.pointerEvents = 'auto'; + this.el.style.opacity = '1'; + this.el.style.cursor = 'pointer'; + this.disabled = false; + } + } } } diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 9e78b8f777..8e05a7c20e 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -3,15 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IOpenerService = createDecorator('openerService'); -type OpenInternalOptions = { +export type OpenInternalOptions = { /** * Signals that the intent is to open an editor to the side @@ -55,7 +56,8 @@ export interface IOpener { } export interface IExternalOpener { - openExternal(href: string): Promise; + openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise; + dispose?(): void; } export interface IValidator { @@ -90,7 +92,12 @@ export interface IOpenerService { * Sets the handler for opening externally. If not provided, * a default handler will be used. */ - setExternalOpener(opener: IExternalOpener): void; + setDefaultExternalOpener(opener: IExternalOpener): void; + + /** + * Registers a new opener external resources openers. + */ + registerExternalOpener(opener: IExternalOpener): IDisposable; /** * Opens a resource, like a webaddress, a document uri, or executes command. @@ -106,15 +113,16 @@ export interface IOpenerService { resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise; } -export const NullOpenerService: IOpenerService = Object.freeze({ +export const NullOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return Disposable.None; }, registerValidator() { return Disposable.None; }, registerExternalUriResolver() { return Disposable.None; }, - setExternalOpener() { }, + setDefaultExternalOpener() { }, + registerExternalOpener() { return Disposable.None; }, async open() { return false; }, async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; }, -}); +} as IOpenerService); export function matchesScheme(target: URI | string, scheme: string) { if (URI.isUri(target)) { diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 08845c8768..0f513d3e91 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -20,16 +20,16 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.17.0-dev', - vscodeVersion: '1.50.0-dev', - nameLong: 'Azure Data Studio Web Dev', - nameShort: 'Azure Data Studio Web Dev', - applicationName: 'ads-oss', - dataFolderName: '.ads-oss', + version: '1.27.0-dev', + vscodeVersion: '1.53.0-dev', + nameLong: isWeb ? 'Azure Data Studio Web Dev' : 'Azure Data Studio Dev', + nameShort: isWeb ? 'Azure Data Studio Web Dev' : 'Azure Data Studio Dev', + applicationName: 'azuredatastudio-oss', + dataFolderName: '.azuredatastudio-oss', urlProtocol: 'azuredatastudio-oss', reportIssueUrl: 'https://github.com/microsoft/azuredatastudio/issues/new', - licenseName: 'Source EULA', - licenseUrl: 'https://github.com/v/vscode/blob/main/LICENSE.txt', + licenseName: 'MIT', + licenseUrl: 'https://github.com/microsoft/azuredatastudio/blob/master/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 670c3314e3..edc5259733 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -135,6 +135,8 @@ export interface IProductConfiguration { readonly linkProtectionTrustedDomains?: readonly string[]; readonly 'configurationSync.store'?: ConfigurationSyncStore; + + readonly darwinUniversalAssetId?: string; } export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; diff --git a/src/vs/platform/registry/test/common/platform.test.ts b/src/vs/platform/registry/test/common/platform.test.ts index 5abce2960b..1b485e4132 100644 --- a/src/vs/platform/registry/test/common/platform.test.ts +++ b/src/vs/platform/registry/test/common/platform.test.ts @@ -2,46 +2,47 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as Platform from 'vs/platform/registry/common/platform'; -import * as Types from 'vs/base/common/types'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { isFunction } from 'vs/base/common/types'; suite('Platform / Registry', () => { test('registry - api', function () { - assert.ok(Types.isFunction(Platform.Registry.add)); - assert.ok(Types.isFunction(Platform.Registry.as)); - assert.ok(Types.isFunction(Platform.Registry.knows)); + assert.ok(isFunction(Registry.add)); + assert.ok(isFunction(Registry.as)); + assert.ok(isFunction(Registry.knows)); }); test('registry - mixin', function () { - Platform.Registry.add('foo', { bar: true }); + Registry.add('foo', { bar: true }); - assert.ok(Platform.Registry.knows('foo')); - assert.ok(Platform.Registry.as('foo').bar); - assert.equal(Platform.Registry.as('foo').bar, true); + assert.ok(Registry.knows('foo')); + assert.ok(Registry.as('foo').bar); + assert.equal(Registry.as('foo').bar, true); }); test('registry - knows, as', function () { let ext = {}; - Platform.Registry.add('knows,as', ext); + Registry.add('knows,as', ext); - assert.ok(Platform.Registry.knows('knows,as')); - assert.ok(!Platform.Registry.knows('knows,as1234')); + assert.ok(Registry.knows('knows,as')); + assert.ok(!Registry.knows('knows,as1234')); - assert.ok(Platform.Registry.as('knows,as') === ext); - assert.ok(Platform.Registry.as('knows,as1234') === null); + assert.ok(Registry.as('knows,as') === ext); + assert.ok(Registry.as('knows,as1234') === null); }); test('registry - mixin, fails on duplicate ids', function () { - Platform.Registry.add('foo-dup', { bar: true }); + Registry.add('foo-dup', { bar: true }); try { - Platform.Registry.add('foo-dup', { bar: false }); + Registry.add('foo-dup', { bar: false }); assert.ok(false); } catch (e) { assert.ok(true); diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index f02da395c5..125e8c8ca7 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { Disposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -86,96 +86,124 @@ export interface ISocketFactory { connect(host: string, port: number, query: string, callback: IConnectCallback): void; } -async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { - const logPrefix = connectLogPrefix(options, connectionType); - const { protocol, ownsProtocol } = await new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { - options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); - options.socketFactory.connect( - options.host, - options.port, - `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, - (err: any, socket: ISocket | undefined) => { - if (err || !socket) { - options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); - options.logService.error(err); - e(err); - return; - } +async function readOneControlMessage(protocol: PersistentProtocol): Promise { + const raw = await Event.toPromise(protocol.onControlMessage); + const msg = JSON.parse(raw.toString()); + const error = getErrorFromMessage(msg); + if (error) { + throw error; + } + return msg; +} - options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); - if (options.reconnectionProtocol) { - options.reconnectionProtocol.beginAcceptReconnection(socket, null); - c({ protocol: options.reconnectionProtocol, ownsProtocol: false }); - } else { - c({ protocol: new PersistentProtocol(socket, null), ownsProtocol: true }); - } +function waitWithTimeout(promise: Promise, timeout: number): Promise { + return new Promise((resolve, reject) => { + const timeoutToken = setTimeout(() => { + const error: any = new Error('Timeout'); + error.code = 'ETIMEDOUT'; + error.syscall = 'connect'; + reject(error); + }, timeout); + + promise.then( + (result) => { + clearTimeout(timeoutToken); + resolve(result); + }, + (error) => { + clearTimeout(timeoutToken); + reject(error); } ); }); +} - return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => { +function createSocket(socketFactory: ISocketFactory, host: string, port: number, query: string): Promise { + return new Promise((resolve, reject) => { + socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => { + if (err || !socket) { + return reject(err); + } + resolve(socket); + }); + }); +} - const errorTimeoutToken = setTimeout(() => { - const error: any = new Error('handshake timeout'); - error.code = 'ETIMEDOUT'; - error.syscall = 'connect'; +async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> { + const logPrefix = connectLogPrefix(options, connectionType); + + options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`); + + let socket: ISocket; + try { + socket = await createSocket(options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`); + } catch (error) { + options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); + options.logService.error(error); + throw error; + } + + options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`); + + let protocol: PersistentProtocol; + let ownsProtocol: boolean; + if (options.reconnectionProtocol) { + options.reconnectionProtocol.beginAcceptReconnection(socket, null); + protocol = options.reconnectionProtocol; + ownsProtocol = false; + } else { + protocol = new PersistentProtocol(socket, null); + ownsProtocol = true; + } + + options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); + const authRequest: AuthRequest = { + type: 'auth', + auth: options.connectionToken || '00000000000000000000' + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); + + try { + const msg = await waitWithTimeout(readOneControlMessage(protocol), 10000); + + if (msg.type !== 'sign' || typeof msg.data !== 'string') { + const error: any = new Error('Unexpected handshake message'); + error.code = 'VSCODE_CONNECTION_ERROR'; + throw error; + } + + options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); + + const signed = await options.signService.sign(msg.data); + const connTypeRequest: ConnectionTypeRequest = { + type: 'connectionType', + commit: options.commit, + signedData: signed, + desiredConnectionType: connectionType + }; + if (args) { + connTypeRequest.args = args; + } + + options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); + + return { protocol, ownsProtocol }; + + } catch (error) { + if (error && error.code === 'ETIMEDOUT') { options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`); options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - e(error); - }, 10000); - - const messageRegistration = protocol.onControlMessage(async raw => { - const msg = JSON.parse(raw.toString()); - // Stop listening for further events - messageRegistration.dispose(); - - const error = getErrorFromMessage(msg); - if (error) { - options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); - options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - return e(error); - } - - if (msg.type === 'sign') { - options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`); - const signed = await options.signService.sign(msg.data); - const connTypeRequest: ConnectionTypeRequest = { - type: 'connectionType', - commit: options.commit, - signedData: signed, - desiredConnectionType: connectionType - }; - if (args) { - connTypeRequest.args = args; - } - options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`); - protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest))); - clearTimeout(errorTimeoutToken); - c({ protocol, ownsProtocol }); - } else { - const error = new Error('handshake error'); - options.logService.error(`${logPrefix} received unexpected control message. Error:`); - options.logService.error(error); - if (ownsProtocol) { - safeDisposeProtocolAndSocket(protocol); - } - e(error); - } - }); - - options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`); - const authRequest: AuthRequest = { - type: 'auth', - auth: options.connectionToken || '00000000000000000000' - }; - protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest))); - }); + } + if (error && error.code === 'VSCODE_CONNECTION_ERROR') { + options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`); + options.logService.error(error); + } + if (ownsProtocol) { + safeDisposeProtocolAndSocket(protocol); + } + throw error; + } } interface IManagementConnectionResult { @@ -287,7 +315,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions, } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err)); throw err; } } @@ -301,7 +329,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err)); throw err; } } @@ -333,10 +361,16 @@ export const enum PersistentConnectionEventType { } export class ConnectionLostEvent { public readonly type = PersistentConnectionEventType.ConnectionLost; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number + ) { } } export class ReconnectionWaitEvent { public readonly type = PersistentConnectionEventType.ReconnectionWait; constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, public readonly durationSeconds: number, private readonly cancellableTimer: CancelablePromise ) { } @@ -347,22 +381,44 @@ export class ReconnectionWaitEvent { } export class ReconnectionRunningEvent { public readonly type = PersistentConnectionEventType.ReconnectionRunning; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number + ) { } } export class ConnectionGainEvent { public readonly type = PersistentConnectionEventType.ConnectionGain; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number + ) { } } export class ReconnectionPermanentFailureEvent { public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure; + constructor( + public readonly reconnectionToken: string, + public readonly millisSinceLastIncomingData: number, + public readonly attempt: number, + public readonly handled: boolean + ) { } } export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent; abstract class PersistentConnection extends Disposable { - public static triggerPermanentFailure(): void { + public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { this._permanentFailure = true; - this._instances.forEach(instance => instance._gotoPermanentFailure()); + this._permanentFailureMillisSinceLastIncomingData = millisSinceLastIncomingData; + this._permanentFailureAttempt = attempt; + this._permanentFailureHandled = handled; + this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled)); } private static _permanentFailure: boolean = false; + private static _permanentFailureMillisSinceLastIncomingData: number = 0; + private static _permanentFailureAttempt: number = 0; + private static _permanentFailureHandled: boolean = false; private static _instances: PersistentConnection[] = []; private readonly _onDidStateChange = this._register(new Emitter()); @@ -381,7 +437,7 @@ abstract class PersistentConnection extends Disposable { this.protocol = protocol; this._isReconnecting = false; - this._onDidStateChange.fire(new ConnectionGainEvent()); + this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0)); this._register(protocol.onSocketClose(() => this._beginReconnecting())); this._register(protocol.onSocketTimeout(() => this._beginReconnecting())); @@ -389,7 +445,7 @@ abstract class PersistentConnection extends Disposable { PersistentConnection._instances.push(this); if (PersistentConnection._permanentFailure) { - this._gotoPermanentFailure(); + this._gotoPermanentFailure(PersistentConnection._permanentFailureMillisSinceLastIncomingData, PersistentConnection._permanentFailureAttempt, PersistentConnection._permanentFailureHandled); } } @@ -413,21 +469,23 @@ abstract class PersistentConnection extends Disposable { } const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true); this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`); - this._onDidStateChange.fire(new ConnectionLostEvent()); - const TIMES = [5, 5, 10, 10, 10, 10, 10, 30]; + this._onDidStateChange.fire(new ConnectionLostEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData())); + const TIMES = [0, 5, 5, 10, 10, 10, 10, 10, 30]; const disconnectStartTime = Date.now(); let attempt = -1; do { attempt++; const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]); try { - const sleepPromise = sleep(waitTime); - this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise)); + if (waitTime > 0) { + const sleepPromise = sleep(waitTime); + this._onDidStateChange.fire(new ReconnectionWaitEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), waitTime, sleepPromise)); - this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); - try { - await sleepPromise; - } catch { } // User canceled timer + this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`); + try { + await sleepPromise; + } catch { } // User canceled timer + } if (PersistentConnection._permanentFailure) { this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`); @@ -435,26 +493,26 @@ abstract class PersistentConnection extends Disposable { } // connection was lost, let's try to re-establish it - this._onDidStateChange.fire(new ReconnectionRunningEvent()); + this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); this._options.logService.info(`${logPrefix} resolving connection...`); const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol); this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`); await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT); this._options.logService.info(`${logPrefix} reconnected!`); - this._onDidStateChange.fire(new ConnectionGainEvent()); + this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); break; } catch (err) { if (err.code === 'VSCODE_CONNECTION_ERROR') { this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } if (Date.now() - disconnectStartTime > ProtocolConstants.ReconnectionGraceTime) { this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time has expired! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) { @@ -475,16 +533,22 @@ abstract class PersistentConnection extends Disposable { // try again! continue; } + if (err instanceof RemoteAuthorityResolverError) { + this._options.logService.error(`${logPrefix} A RemoteAuthorityResolverError occurred while trying to reconnect. Will give up now! Error:`); + this._options.logService.error(err); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, RemoteAuthorityResolverError.isHandled(err)); + break; + } this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); this._options.logService.error(err); - PersistentConnection.triggerPermanentFailure(); + PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } } while (!PersistentConnection._permanentFailure); } - private _gotoPermanentFailure(): void { - this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent()); + private _gotoPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { + this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent(this.reconnectionToken, millisSinceLastIncomingData, attempt, handled)); safeDisposeProtocolAndSocket(this.protocol); } diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index 98b5aa3a07..597029a4c7 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -5,6 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; +import * as performance from 'vs/base/common/performance'; export interface IRemoteAgentEnvironment { pid: number; @@ -18,6 +19,7 @@ export interface IRemoteAgentEnvironment { workspaceStorageHome: URI; userHome: URI; os: OperatingSystem; + marks: performance.PerformanceMark[]; useHostProxy: boolean; } diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index 2714643727..f8f723d63b 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -19,7 +19,7 @@ export function getRemoteName(authority: string | undefined): string | undefined } const pos = authority.indexOf('+'); if (pos < 0) { - // funky? bad authority? + // e.g. localhost:8000 return authority; } return authority.substr(0, pos); diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index db7fa2400a..0697e7ad98 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { isWindows, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -17,36 +18,64 @@ export interface RemoteTunnel { readonly tunnelRemoteHost: string; readonly tunnelLocalPort?: number; readonly localAddress: string; - dispose(silent?: boolean): void; + readonly public: boolean; + dispose(silent?: boolean): Promise; } export interface TunnelOptions { - remoteAddress: { port: number, host: string }; + remoteAddress: { port: number, host: string; }; localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelCreationOptions { elevationRequired?: boolean; } +export interface TunnelProviderFeatures { + elevation: boolean; + public: boolean; +} + export interface ITunnelProvider { - forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; + forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; +} + +export interface ITunnel { + remoteAddress: { port: number, host: string }; + + /** + * The complete local address(ex. localhost:1234) + */ + localAddress: string; + + public?: boolean; + + /** + * Implementers of Tunnel should fire onDidDispose when dispose is called. + */ + onDidDispose: Event; + + dispose(): Promise | void; } export interface ITunnelService { readonly _serviceBrand: undefined; readonly tunnels: Promise; + readonly canMakePublic: boolean; readonly onTunnelOpened: Event; - readonly onTunnelClosed: Event<{ host: string, port: number }>; + readonly onTunnelClosed: Event<{ host: string, port: number; }>; + readonly canElevate: boolean; - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; + canTunnel(uri: URI): boolean; + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise | undefined; closeTunnel(remoteHost: string, remotePort: number): Promise; - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable; } -export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined { +export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number; } | undefined { if (uri.scheme !== 'http' && uri.scheme !== 'https') { return undefined; } @@ -74,51 +103,88 @@ function getOtherLocalhost(host: string): string | undefined { return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined); } +export function isPortPrivileged(port: number, os?: OperatingSystem): boolean { + if (os) { + return os !== OperatingSystem.Windows && (port < 1024); + } else { + return !isWindows && (port < 1024); + } +} + export abstract class AbstractTunnelService implements ITunnelService { declare readonly _serviceBrand: undefined; private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - protected readonly _tunnels = new Map }>>(); + private _onTunnelClosed: Emitter<{ host: string, port: number; }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number; }> = this._onTunnelClosed.event; + protected readonly _tunnels = new Map; }>>(); protected _tunnelProvider: ITunnelProvider | undefined; + protected _canElevate: boolean = false; + private _canMakePublic: boolean = false; public constructor( @ILogService protected readonly logService: ILogService ) { } - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable { + this._tunnelProvider = provider; if (!provider) { + // clear features + this._canElevate = false; + this._canMakePublic = false; return { dispose: () => { } }; } - this._tunnelProvider = provider; + this._canElevate = features.elevation; + this._canMakePublic = features.public; return { dispose: () => { this._tunnelProvider = undefined; + this._canElevate = false; + this._canMakePublic = false; } }; } - public get tunnels(): Promise { - const promises: Promise[] = []; - Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); - return Promise.all(promises); + public get canElevate(): boolean { + return this._canElevate; } - dispose(): void { + public get canMakePublic() { + return this._canMakePublic; + } + + public get tunnels(): Promise { + return new Promise(async (resolve) => { + const tunnels: RemoteTunnel[] = []; + const tunnelArray = Array.from(this._tunnels.values()); + for (let portMap of tunnelArray) { + const portArray = Array.from(portMap.values()); + for (let x of portArray) { + const tunnelValue = await x.value; + if (tunnelValue) { + tunnels.push(tunnelValue); + } + } + } + resolve(tunnels); + }); + } + + async dispose(): Promise { for (const portMap of this._tunnels.values()) { for (const { value } of portMap.values()) { - value.then(tunnel => tunnel.dispose()); + await value.then(tunnel => tunnel?.dispose()); } portMap.clear(); } this._tunnels.clear(); } - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise | undefined { + this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); if (!addressProvider) { return undefined; } @@ -127,12 +193,19 @@ export abstract class AbstractTunnelService implements ITunnelService { remoteHost = 'localhost'; } - const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort); + const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); if (!resolvedTunnel) { + this.logService.trace(`Tunnel was not created.`); return resolvedTunnel; } return resolvedTunnel.then(tunnel => { + if (!tunnel) { + this.logService.trace('New tunnel is undefined.'); + this.removeEmptyTunnelFromMap(remoteHost!, remotePort); + return undefined; + } + this.logService.trace('New tunnel established.'); const newTunnel = this.makeTunnel(tunnel); if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) { this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.'); @@ -148,24 +221,28 @@ export abstract class AbstractTunnelService implements ITunnelService { tunnelRemoteHost: tunnel.tunnelRemoteHost, tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, - dispose: () => { + public: tunnel.public, + dispose: async () => { const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); if (existingHost) { const existing = existingHost.get(tunnel.tunnelRemotePort); if (existing) { existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); + await this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); } } } }; } - private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { if (tunnel.refcount <= 0) { - const disposePromise: Promise = tunnel.value.then(tunnel => { - tunnel.dispose(true); - this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`); + const disposePromise: Promise = tunnel.value.then(async (tunnel) => { + if (tunnel) { + await tunnel.dispose(true); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + } }); if (this._tunnels.has(remoteHost)) { this._tunnels.get(remoteHost)!.delete(remotePort); @@ -183,16 +260,30 @@ export abstract class AbstractTunnelService implements ITunnelService { } } - protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { if (!this._tunnels.has(remoteHost)) { this._tunnels.set(remoteHost, new Map()); } this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); } - protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { + private async removeEmptyTunnelFromMap(remoteHost: string, remotePort: number) { + const hostMap = this._tunnels.get(remoteHost); + if (hostMap) { + const tunnel = hostMap.get(remotePort); + const tunnelResult = await tunnel; + if (!tunnelResult) { + hostMap.delete(remotePort); + } + if (hostMap.size === 0) { + this._tunnels.delete(remoteHost); + } + } + } + + protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { const otherLocalhost = getOtherLocalhost(remoteHost); - let portMap: Map }> | undefined; + let portMap: Map }> | undefined; if (otherLocalhost) { const firstMap = this._tunnels.get(remoteHost); const secondMap = this._tunnels.get(otherLocalhost); @@ -207,31 +298,25 @@ export abstract class AbstractTunnelService implements ITunnelService { return portMap ? portMap.get(remotePort) : undefined; } - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; + canTunnel(uri: URI): boolean { + return !!extractLocalHostUriMetaDataForPortMapping(uri); + } - protected isPortPrivileged(port: number): boolean { - return port < 1024; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined; + + protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { + this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`); + + const preferredLocalPort = localPort === undefined ? remotePort : localPort; + const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; + const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic }; + const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); + this.logService.trace('Tunnel created by provider.'); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; } } -export class TunnelService extends AbstractTunnelService { - protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const existing = this.getTunnelFromMap(remoteHost, remotePort); - if (existing) { - ++existing.refcount; - return existing.value; - } - if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; - } - return undefined; - } -} diff --git a/src/vs/platform/remote/node/nodeSocketFactory.ts b/src/vs/platform/remote/node/nodeSocketFactory.ts index 6447c7201c..62682e991b 100644 --- a/src/vs/platform/remote/node/nodeSocketFactory.ts +++ b/src/vs/platform/remote/node/nodeSocketFactory.ts @@ -22,7 +22,7 @@ export const nodeSocketFactory = new class implements ISocketFactory { const nonce = buffer.toString('base64'); let headers = [ - `GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, + `GET ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`, `Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Key: ${nonce}` diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 90f8ebee06..18275ea0d9 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -10,7 +10,7 @@ import { findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; +import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -26,6 +26,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public tunnelLocalPort!: number; public tunnelRemoteHost: string; public localAddress!: string; + public readonly public = false; private readonly _options: IConnectionOptions; private readonly _server: net.Server; @@ -57,7 +58,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelRemoteHost = tunnelRemoteHost; } - public dispose(): void { + public async dispose(): Promise { super.dispose(); this._server.removeListener('listening', this._listeningListener); this._server.removeListener('connection', this._connectionListener); @@ -129,8 +130,9 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { } } -export class TunnelService extends AbstractTunnelService { +export class BaseTunnelService extends AbstractTunnelService { public constructor( + private readonly socketFactory: ISocketFactory, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, @IProductService private readonly productService: IProductService @@ -138,7 +140,7 @@ export class TunnelService extends AbstractTunnelService { super(logService); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; @@ -146,18 +148,12 @@ export class TunnelService extends AbstractTunnelService { } if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) }; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; + return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); } else { + this.logService.trace(`Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const options: IConnectionOptions = { commit: this.productService.commit, - socketFactory: nodeSocketFactory, + socketFactory: this.socketFactory, addressProvider, signService: this.signService, logService: this.logService, @@ -165,8 +161,19 @@ export class TunnelService extends AbstractTunnelService { }; const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); + this.logService.trace('Tunnel created without provider.'); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; } } } + +export class TunnelService extends BaseTunnelService { + public constructor( + @ILogService logService: ILogService, + @ISignService signService: ISignService, + @IProductService productService: IProductService + ) { + super(nodeSocketFactory, logService, signService, productService); + } +} diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts new file mode 100644 index 0000000000..d24cbeff30 --- /dev/null +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { LogLevel } from 'vs/platform/log/common/log'; + +export interface ISharedProcess { + + /** + * Toggles the visibility of the otherwise hidden + * shared process window. + */ + toggle(): Promise; +} + +export interface ISharedProcessConfiguration { + readonly machineId: string; + readonly windowId: number; + + readonly appRoot: string; + + readonly userEnv: NodeJS.ProcessEnv; + + readonly sharedIPCHandle: string; + + readonly args: NativeParsedArgs; + + readonly logLevel: LogLevel; + + readonly nodeCachedDataDir?: string; + readonly backupWorkspacesPath: string; +} diff --git a/src/vs/platform/state/node/state.ts b/src/vs/platform/state/node/state.ts index b9233afe1e..1310ab5cfd 100644 --- a/src/vs/platform/state/node/state.ts +++ b/src/vs/platform/state/node/state.ts @@ -12,6 +12,8 @@ export interface IStateService { getItem(key: string, defaultValue: T): T; getItem(key: string, defaultValue?: T): T | undefined; + setItem(key: string, data?: object | string | number | boolean | undefined | null): void; + removeItem(key: string): void; } diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index d16f9f47f3..c5e29ac11e 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -6,7 +6,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { writeFileSync, readFile } from 'vs/base/node/pfs'; +import { writeFileSync } from 'vs/base/node/pfs'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { IStateService } from 'vs/platform/state/node/state'; import { ILogService } from 'vs/platform/log/common/log'; @@ -58,7 +58,7 @@ export class FileStorage { private async loadAsync(): Promise { try { - this.lastFlushedSerializedDatabase = (await readFile(this.dbPath)).toString(); + this.lastFlushedSerializedDatabase = (await fs.promises.readFile(this.dbPath)).toString(); return JSON.parse(this.lastFlushedSerializedDatabase); } catch (error) { @@ -143,7 +143,7 @@ export class StateService implements IStateService { } getItem(key: string, defaultValue: T): T; - getItem(key: string, defaultValue: T | undefined): T | undefined; + getItem(key: string, defaultValue?: T): T | undefined; getItem(key: string, defaultValue?: T): T | undefined { return this.fileStorage.getItem(key, defaultValue); } diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index b84d83074a..fedba5a02b 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -4,31 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { tmpdir } from 'os'; +import { promises } from 'fs'; +import { join } from 'vs/base/common/path'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileStorage } from 'vs/platform/state/node/stateService'; -import { mkdirp, rimraf, RimRafMode, writeFileSync } from 'vs/base/node/pfs'; +import { rimraf, writeFileSync } from 'vs/base/node/pfs'; -suite('StateService', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'stateservice'); - const storageFile = path.join(parentDir, 'storage.json'); +flakySuite('StateService', () => { - teardown(async () => { - await rimraf(parentDir, RimRafMode.MOVE); + let testDir: string; + + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice'); + + return promises.mkdir(testDir, { recursive: true }); }); - test('Basics', async () => { - await mkdirp(parentDir); + teardown(() => { + return rimraf(testDir); + }); + + test('Basics', async function () { + const storageFile = join(testDir, 'storage.json'); writeFileSync(storageFile, ''); let service = new FileStorage(storageFile, () => null); service.setItem('some.key', 'some.value'); - assert.equal(service.getItem('some.key'), 'some.value'); + assert.strictEqual(service.getItem('some.key'), 'some.value'); service.removeItem('some.key'); - assert.equal(service.getItem('some.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default'); assert.ok(!service.getItem('some.unknonw.key')); @@ -36,15 +43,15 @@ suite('StateService', () => { service = new FileStorage(storageFile, () => null); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); service.setItem('some.other.key', 'some.other.value'); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.strictEqual(service.getItem('some.other.key'), 'some.other.value'); service.setItem('some.undefined.key', undefined); - assert.equal(service.getItem('some.undefined.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default'); service.setItem('some.null.key', null); - assert.equal(service.getItem('some.null.key', 'some.default'), 'some.default'); + assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 18a9d241f1..6005a112da 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -12,7 +12,7 @@ import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; -import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async'; +import { runWhenIdle, RunOnceScheduler, Promises } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; @@ -71,7 +71,7 @@ export class BrowserStorageService extends AbstractStorageService { this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key))); // Init both - await Promise.all([ + await Promises.settled([ this.workspaceStorage.init(), this.globalStorage.init() ]); @@ -146,7 +146,7 @@ export class BrowserStorageService extends AbstractStorageService { } protected async doFlush(): Promise { - await Promise.all([ + await Promises.settled([ this.getStorage(StorageScope.GLOBAL).whenFlushed(), this.getStorage(StorageScope.WORKSPACE).whenFlushed() ]); diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 5af383992c..19bbc1c34a 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -200,12 +200,10 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS return this.channel.call('updateItems', serializableRequest); } - close(): Promise { + async close(): Promise { // when we are about to close, we start to ignore main-side changes since we close anyway dispose(this.onDidChangeItemsOnMainListener); - - return Promise.resolve(); // global storage is closed on the main side } dispose(): void { diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 5f29ac509e..28f099da70 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +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'; @@ -10,11 +11,11 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/ import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage'; import { mark } from 'vs/base/common/performance'; import { join } from 'vs/base/common/path'; -import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs'; +import { copy, exists, writeFile } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { assertIsDefined } from 'vs/base/common/types'; -import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; +import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; export class NativeStorageService extends AbstractStorageService { @@ -59,7 +60,7 @@ export class NativeStorageService extends AbstractStorageService { private async doInitialize(payload?: IWorkspaceInitializationPayload): Promise { // Init all storage locations - await Promise.all([ + await Promises.settled([ this.initializeGlobalStorage(), payload ? this.initializeWorkspaceStorage(payload) : Promise.resolve() ]); @@ -83,7 +84,7 @@ export class NativeStorageService extends AbstractStorageService { const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests! // Create workspace storage and initialize - mark('willInitWorkspaceStorage'); + mark('code/willInitWorkspaceStorage'); try { const workspaceStorage = this.createWorkspaceStorage( useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME), @@ -99,7 +100,7 @@ export class NativeStorageService extends AbstractStorageService { workspaceStorage.set(IS_NEW_KEY, false); } } finally { - mark('didInitWorkspaceStorage'); + mark('code/didInitWorkspaceStorage'); } } catch (error) { this.logService.error(`[storage] initializeWorkspaceStorage(): Unable to init workspace storage due to ${error}`); @@ -138,7 +139,7 @@ export class NativeStorageService extends AbstractStorageService { return { path: workspaceStorageFolderPath, wasCreated: false }; } - await mkdirp(workspaceStorageFolderPath); + await promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder this.ensureWorkspaceStorageFolderMeta(payload); @@ -148,23 +149,22 @@ export class NativeStorageService extends AbstractStorageService { private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void { let meta: object | undefined = undefined; - if (isSingleFolderWorkspaceInitializationPayload(payload)) { - meta = { folder: payload.folder.toString() }; + if (isSingleFolderWorkspaceIdentifier(payload)) { + meta = { folder: payload.uri.toString() }; } else if (isWorkspaceIdentifier(payload)) { - meta = { configuration: payload.configPath }; + meta = { workspace: payload.configPath.toString() }; } if (meta) { - const logService = this.logService; - const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); - (async function () { + (async () => { try { + const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME); const storageExists = await exists(workspaceStorageMetaPath); if (!storageExists) { await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); } } catch (error) { - logService.error(error); + this.logService.error(error); } })(); } @@ -210,7 +210,7 @@ export class NativeStorageService extends AbstractStorageService { promises.push(this.workspaceStorage.whenFlushed()); } - await Promise.all(promises); + await Promises.settled(promises); } private doFlushWhenIdle(): void { @@ -240,7 +240,7 @@ export class NativeStorageService extends AbstractStorageService { this.emitWillSaveState(WillSaveStateReason.SHUTDOWN); // Do it - await Promise.all([ + await Promises.settled([ this.globalStorage.close(), this.workspaceStorage ? this.workspaceStorage.close() : Promise.resolve() ]); @@ -268,7 +268,7 @@ export class NativeStorageService extends AbstractStorageService { const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME); // Copy current storage over to new workspace storage - await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath); + 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/test/common/storageService.test.ts b/src/vs/platform/storage/test/common/storageService.test.ts index 85a4277218..e4c5d64712 100644 --- a/src/vs/platform/storage/test/common/storageService.test.ts +++ b/src/vs/platform/storage/test/common/storageService.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { strictEqual, ok, equal } from 'assert'; +import { strictEqual, ok } from 'assert'; import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage'; suite('StorageService', function () { @@ -32,15 +32,15 @@ suite('StorageService', function () { storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE); strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar'); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.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)!), ''); storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get'); - equal(storageValueChangeEvent!.scope, scope); - equal(storageValueChangeEvent!.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); @@ -79,8 +79,8 @@ suite('StorageService', function () { storage.remove('test.remove', scope); ok(!storage.get('test.remove', scope, (undefined)!)); let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.remove'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.remove'); } test('Keys (in-memory)', () => { @@ -107,20 +107,20 @@ suite('StorageService', function () { storage.store('test.target1', 'value1', scope, target); strictEqual(storage.keys(scope, target).length, 1); - equal(storageTargetEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.target1'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.target, target); + strictEqual(storageTargetEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.target1'); + strictEqual(storageValueChangeEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.target, target); storageTargetEvent = undefined; storageValueChangeEvent = Object.create(null); storage.store('test.target1', 'otherValue1', scope, target); strictEqual(storage.keys(scope, target).length, 1); - equal(storageTargetEvent, undefined); - equal(storageValueChangeEvent?.key, 'test.target1'); - equal(storageValueChangeEvent?.scope, scope); - equal(storageValueChangeEvent?.target, target); + 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); @@ -142,9 +142,9 @@ suite('StorageService', function () { storage.remove('test.target4', scope); strictEqual(storage.keys(scope, target).length, keysLength); - equal(storageTargetEvent?.scope, scope); - equal(storageValueChangeEvent?.key, 'test.target4'); - equal(storageValueChangeEvent?.scope, scope); + strictEqual(storageTargetEvent?.scope, scope); + strictEqual(storageValueChangeEvent?.key, 'test.target4'); + strictEqual(storageValueChangeEvent?.scope, scope); } } @@ -171,7 +171,7 @@ suite('StorageService', function () { storage.store('test.target1', undefined, scope, target); strictEqual(storage.keys(scope, target).length, 0); - equal(storageTargetEvent?.scope, scope); + strictEqual(storageTargetEvent?.scope, scope); storage.store('test.target1', '', scope, target); strictEqual(storage.keys(scope, target).length, 1); diff --git a/src/vs/platform/storage/test/electron-browser/storage.test.ts b/src/vs/platform/storage/test/electron-browser/storage.test.ts index 14e208a060..7aa5de5d0a 100644 --- a/src/vs/platform/storage/test/electron-browser/storage.test.ts +++ b/src/vs/platform/storage/test/electron-browser/storage.test.ts @@ -3,12 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { strictEqual } from 'assert'; import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService'; -import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { Storage } from 'vs/base/parts/storage/common/storage'; import { URI } from 'vs/base/common/uri'; @@ -20,32 +19,28 @@ import { Schemas } from 'vs/base/common/network'; suite('Storage', () => { - const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); + let testDir: string; let fileService: FileService; let fileProvider: DiskFileSystemProvider; - let testDir: string; const disposables = new DisposableStore(); setup(async () => { const logService = new NullLogService(); - fileService = new FileService(logService); - disposables.add(fileService); + fileService = disposables.add(new FileService(logService)); - fileProvider = new DiskFileSystemProvider(logService); + fileProvider = disposables.add(new DiskFileSystemProvider(logService)); disposables.add(fileService.registerProvider(Schemas.file, fileProvider)); - disposables.add(fileProvider); - const id = generateUuid(); - testDir = join(parentDir, id); + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); }); - teardown(async () => { + teardown(() => { disposables.clear(); - await rimraf(parentDir, RimRafMode.MOVE); + return rimraf(testDir); }); test('File Based Storage', async () => { @@ -57,9 +52,9 @@ suite('Storage', () => { storage.set('barNumber', 55); storage.set('barBoolean', true); - equal(storage.get('bar'), 'foo'); - equal(storage.get('barNumber'), '55'); - equal(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); await storage.close(); @@ -67,17 +62,17 @@ suite('Storage', () => { await storage.init(); - equal(storage.get('bar'), 'foo'); - equal(storage.get('barNumber'), '55'); - equal(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); storage.delete('bar'); storage.delete('barNumber'); storage.delete('barBoolean'); - equal(storage.get('bar', 'undefined'), 'undefined'); - equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); - equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); await storage.close(); @@ -85,8 +80,8 @@ suite('Storage', () => { await storage.init(); - equal(storage.get('bar', 'undefined'), 'undefined'); - equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); - equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); }); }); diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index ff5e9a20d0..ec463b2950 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -3,33 +3,34 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equal } from 'assert'; +import { strictEqual } from 'assert'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { promises } from 'fs'; +import { rimraf } from 'vs/base/node/pfs'; import { NullLogService } from 'vs/platform/log/common/log'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; 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'; -suite('NativeStorageService', function () { +flakySuite('NativeStorageService', function () { - function uniqueStorageDir(): string { - const id = generateUuid(); + let testDir: string; - return join(tmpdir(), 'vsctests', id, 'storage2', id); - } + setup(() => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice'); - test('Migrate Data', async () => { + return promises.mkdir(testDir, { recursive: true }); + }); - // Given issues such as https://github.com/microsoft/vscode/issues/108113 - // we see random test failures when accessing the native file system. - this.retries(3); - this.timeout(1000 * 20); + teardown(() => { + return rimraf(testDir); + }); + + test('Migrate Data', async function () { class StorageTestEnvironmentService extends NativeEnvironmentService { @@ -46,27 +47,23 @@ suite('NativeStorageService', function () { } } - const storageDir = uniqueStorageDir(); - await mkdirp(storageDir); - - const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir)); + const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir)); await storage.initialize({ id: String(Date.now()) }); storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE); storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE); - equal(storage.get('bar', StorageScope.WORKSPACE), 'foo'); - equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); - equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); + strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo'); + strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); + strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.migrate({ id: String(Date.now() + 100) }); - equal(storage.get('bar', StorageScope.WORKSPACE), 'foo'); - equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); - equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); + strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo'); + strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55); + strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true); await storage.close(); - await rimraf(storageDir, RimRafMode.MOVE); }); }); diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts similarity index 80% rename from src/vs/platform/telemetry/node/commonProperties.ts rename to src/vs/platform/telemetry/common/commonProperties.ts index 5de092f787..f8fb9ed48a 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -3,15 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as Platform from 'vs/base/common/platform'; -import * as os from 'os'; -import * as uuid from 'vs/base/common/uuid'; -import { readFile } from 'vs/base/node/pfs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { isLinuxSnap, PlatformToString, platform } from 'vs/base/common/platform'; +import { platform as nodePlatform, env } from 'vs/base/common/process'; +import { generateUuid } from 'vs/base/common/uuid'; +import { URI } from 'vs/base/common/uri'; import product from 'vs/platform/product/common/product'; // {{SQL CARBON EDIT}} const productObject = product; // {{SQL CARBON EDIT}} export async function resolveCommonProperties( + fileService: IFileService, + release: string, + arch: string, commit: string | undefined, version: string | undefined, machineId: string | undefined, @@ -23,19 +27,19 @@ export async function resolveCommonProperties( // __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } result['common.machineId'] = machineId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['sessionID'] = uuid.generateUuid() + Date.now(); + result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['commitHash'] = commit; // __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['version'] = version; // __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platformVersion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); + result['common.platformVersion'] = (release || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); // __GDPR__COMMON__ "common.platform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.platform'] = Platform.PlatformToString(Platform.platform); + result['common.platform'] = PlatformToString(platform); // __GDPR__COMMON__ "common.nodePlatform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.nodePlatform'] = process.platform; + result['common.nodePlatform'] = nodePlatform; // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.nodeArch'] = process.arch; + result['common.nodeArch'] = arch; // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.product'] = productObject.nameShort || 'desktop'; // {{SQL CARBON EDIT}} result['common.application.name'] = productObject.nameLong; // {{SQL CARBON EDIT}} @@ -68,16 +72,16 @@ export async function resolveCommonProperties( } }); - if (process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION) { + if (isLinuxSnap) { // __GDPR__COMMON__ "common.snap" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.snap'] = 'true'; } try { - const contents = await readFile(installSourcePath, 'utf8'); + const contents = await fileService.readFile(URI.file(installSourcePath)); // __GDPR__COMMON__ "common.source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - result['common.source'] = contents.slice(0, 30); + result['common.source'] = contents.value.toString().slice(0, 30); } catch (error) { // ignore error } @@ -86,10 +90,11 @@ export async function resolveCommonProperties( } function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean { - if (!process || !process.env || !process.env['USERDNSDOMAIN']) { + const userDnsDomain = env['USERDNSDOMAIN']; + if (!userDnsDomain) { return false; } - const domain = process.env['USERDNSDOMAIN']!.toLowerCase(); + const domain = userDnsDomain.toLowerCase(); return domainList.some(msftDomain => domain === msftDomain); } diff --git a/src/vs/platform/telemetry/node/telemetryIpc.ts b/src/vs/platform/telemetry/common/telemetryIpc.ts similarity index 100% rename from src/vs/platform/telemetry/node/telemetryIpc.ts rename to src/vs/platform/telemetry/common/telemetryIpc.ts diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 0fdf304d0a..2d64eaa22a 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -9,6 +9,7 @@ import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/t import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { safeStringify } from 'vs/base/common/objects'; import { isObject } from 'vs/base/common/types'; +import { Promises } from 'vs/base/common/async'; export const NullTelemetryService = new class implements ITelemetryService { declare readonly _serviceBrand: undefined; @@ -47,7 +48,7 @@ export interface ITelemetryAppender { export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender { return { log: (e, d) => appenders.forEach(a => a.log(e, d)), - flush: () => Promise.all(appenders.map(a => a.flush())) + flush: () => Promises.settled(appenders.map(a => a.flush())) }; } diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts new file mode 100644 index 0000000000..0b068bdb35 --- /dev/null +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { getIconRegistry, IconContribution, IconFontContribution } from 'vs/platform/theme/common/iconRegistry'; +import { asCSSPropertyValue, asCSSUrl } from 'vs/base/browser/dom'; +import { Event, Emitter } from 'vs/base/common/event'; + + +export interface IIconsStyleSheet { + getCSS(): string; + readonly onDidChange: Event; +} + +export function getIconsStyleSheet(): IIconsStyleSheet { + const onDidChangeEmmiter = new Emitter(); + const iconRegistry = getIconRegistry(); + iconRegistry.onDidChange(() => onDidChangeEmmiter.fire()); + + return { + onDidChange: onDidChangeEmmiter.event, + getCSS() { + const usedFontIds: { [id: string]: IconFontContribution } = {}; + const formatIconRule = (contribution: IconContribution): string | undefined => { + let definition = contribution.defaults; + while (ThemeIcon.isThemeIcon(definition)) { + const c = iconRegistry.getIcon(definition.id); + if (!c) { + return undefined; + } + definition = c.defaults; + } + const fontId = definition.fontId; + if (fontId) { + const fontContribution = iconRegistry.getIconFont(fontId); + if (fontContribution) { + usedFontIds[fontId] = fontContribution; + return `.codicon-${contribution.id}:before { content: '${definition.character}'; font-family: ${asCSSPropertyValue(fontId)}; }`; + } + } + return `.codicon-${contribution.id}:before { content: '${definition.character}'; }`; + }; + + const rules = []; + for (let contribution of iconRegistry.getIcons()) { + const rule = formatIconRule(contribution); + if (rule) { + rules.push(rule); + } + } + for (let id in usedFontIds) { + const fontContribution = usedFontIds[id]; + const src = fontContribution.definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', '); + rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)}; }`); + } + return rules.join('\n'); + } + }; +} diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 45c969c981..985baa64f5 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -344,6 +344,12 @@ export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.s */ export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hc: Color.cyan }, nls.localize('activeLinkForeground', 'Color of active links.')); +/** + * Inline hints + */ +export const editorInlineHintForeground = registerColor('editorInlineHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlineHintForeground', 'Foreground color of inline hints')); +export const editorInlineHintBackground = registerColor('editorInlineHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlineHintBackground', 'Background color of inline hints')); + /** * Editor lighbulb icon colors */ @@ -370,7 +376,7 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: */ 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 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 listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0074E8', 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 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.")); diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index 84e9ff29a7..2c9df9ad3a 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -11,12 +11,11 @@ import { localize } from 'vs/nls'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as Codicons from 'vs/base/common/codicons'; - +import { URI } from 'vs/base/common/uri'; // ------ API types - -// color registry +// icon registry export const Extensions = { IconContribution: 'base.contributions.icons' }; @@ -35,6 +34,15 @@ export interface IconContribution { defaults: IconDefaults; } +export interface IconFontContribution { + id: string; + definition: IconFontDefinition; +} + +export interface IconFontDefinition { + src: { location: URI, format: string; }[] +} + export interface IIconRegistry { readonly onDidChange: Event; @@ -43,12 +51,12 @@ export interface IIconRegistry { * Register a icon to the registry. * @param id The icon id * @param defaults The default values - * @description the description + * @param description The description */ registerIcon(id: string, defaults: IconDefaults, description?: string): ThemeIcon; /** - * Register a icon to the registry. + * Deregister a icon from the registry. */ deregisterIcon(id: string): void; @@ -63,7 +71,7 @@ export interface IIconRegistry { getIcon(id: string): IconContribution | undefined; /** - * JSON schema for an object to assign icon values to one of the color contributions. + * JSON schema for an object to assign icon values to one of the icon contributions. */ getIconSchema(): IJSONSchema; @@ -73,10 +81,26 @@ export interface IIconRegistry { getIconReferenceSchema(): IJSONSchema; /** - * The CSS for all icons + * Register a icon font to the registry. + * @param id The icon font id + * @param definition The iocn font definition */ - getCSS(): string; + registerIconFont(id: string, definition: IconFontDefinition): IconFontContribution; + /** + * Deregister an icon font to the registry. + */ + deregisterIconFont(id: string): void; + + /** + * Get all icon font contributions + */ + getIconFonts(): IconFontContribution[]; + + /** + * Get the icon font for the given id + */ + getIconFont(id: string): IconFontContribution | undefined; } class IconRegistry implements IIconRegistry { @@ -100,10 +124,13 @@ class IconRegistry implements IIconRegistry { type: 'object', properties: {} }; - private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; + private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', pattern: `^${Codicons.CSSIcon.iconNameExpression}$`, enum: [], enumDescriptions: [] }; + + private iconFontsById: { [key: string]: IconFontContribution }; constructor() { this.iconsById = {}; + this.iconFontsById = {}; } public registerIcon(id: string, defaults: IconDefaults, description?: string, deprecationMessage?: string): ThemeIcon { @@ -111,7 +138,7 @@ class IconRegistry implements IIconRegistry { if (existing) { if (description && !existing.description) { existing.description = description; - this.iconSchema.properties[id].markdownDescription = `${description}: $(${id})`; + this.iconSchema.properties[id].markdownDescription = `${description} $(${id})`; const enumIndex = this.iconReferenceSchema.enum.indexOf(id); if (enumIndex !== -1) { this.iconReferenceSchema.enumDescriptions[enumIndex] = description; @@ -165,36 +192,31 @@ class IconRegistry implements IIconRegistry { return this.iconReferenceSchema; } - public getCSS() { - const rules = []; - for (let id in this.iconsById) { - const rule = this.formatRule(id); - if (rule) { - rules.push(rule); - } + public registerIconFont(id: string, definition: IconFontDefinition): IconFontContribution { + const existing = this.iconFontsById[id]; + if (existing) { + return existing; } - return rules.join('\n'); + let iconFontContribution: IconFontContribution = { id, definition }; + this.iconFontsById[id] = iconFontContribution; + this._onDidChange.fire(); + return iconFontContribution; } - private formatRule(id: string): string | undefined { - let definition = this.iconsById[id].defaults; - while (ThemeIcon.isThemeIcon(definition)) { - const c = this.iconsById[definition.id]; - if (!c) { - return undefined; - } - definition = c.defaults; - } - return `.codicon-${id}:before { content: '${definition.character}'; }`; + public deregisterIconFont(id: string): void { + delete this.iconFontsById[id]; + } + + public getIconFonts(): IconFontContribution[] { + return Object.keys(this.iconFontsById).map(id => this.iconFontsById[id]); + } + + public getIconFont(id: string): IconFontContribution | undefined { + return this.iconFontsById[id]; } public toString() { const sorter = (i1: IconContribution, i2: IconContribution) => { - const isThemeIcon1 = ThemeIcon.isThemeIcon(i1.defaults); - const isThemeIcon2 = ThemeIcon.isThemeIcon(i2.defaults); - if (isThemeIcon1 !== isThemeIcon2) { - return isThemeIcon1 ? -1 : 1; - } return i1.id.localeCompare(i2.id); }; const classNames = (i: IconContribution) => { @@ -205,18 +227,24 @@ class IconRegistry implements IIconRegistry { }; let reference = []; - let docCss = []; + reference.push(`| preview | identifier | default codicon ID | description`); + reference.push(`| ----------- | --------------------------------- | --------------------------------- | --------------------------------- |`); const contributions = Object.keys(this.iconsById).map(key => this.iconsById[key]); - for (const i of contributions.sort(sorter)) { - reference.push(`||${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : ''}|${i.description || ''}|`); - - if (!ThemeIcon.isThemeIcon((i.defaults))) { - docCss.push(`.codicon-${i.id}:before { content: "${i.defaults.character}" }`); - } + for (const i of contributions.filter(i => !!i.description).sort(sorter)) { + reference.push(`||${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : i.id}|${i.description || ''}|`); } - return reference.join('\n') + '\n\n' + docCss.join('\n'); + + reference.push(`| preview | identifier `); + reference.push(`| ----------- | --------------------------------- |`); + + for (const i of contributions.filter(i => !ThemeIcon.isThemeIcon(i.defaults)).sort(sorter)) { + reference.push(`||${i.id}|`); + + } + + return reference.join('\n'); } } @@ -262,3 +290,5 @@ export const widgetClose = registerIcon('widget-close', Codicons.Codicon.close, export const gotoPreviousLocation = registerIcon('goto-previous-location', Codicons.Codicon.arrowUp, localize('previousChangeIcon', 'Icon for goto previous editor location.')); export const gotoNextLocation = registerIcon('goto-next-location', Codicons.Codicon.arrowDown, localize('nextChangeIcon', 'Icon for goto next editor location.')); + +export const syncing = ThemeIcon.modify(Codicons.Codicon.sync, 'spin'); diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index 16fa98cf74..dad895b559 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -11,7 +11,7 @@ import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Event, Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { CSSIcon } from 'vs/base/common/codicons'; +import { Codicon, CSSIcon } from 'vs/base/common/codicons'; export const IThemeService = createDecorator('themeService'); @@ -40,18 +40,15 @@ export namespace ThemeIcon { return obj && typeof obj === 'object' && typeof (obj).id === 'string' && (typeof (obj).color === 'undefined' || ThemeColor.isThemeColor((obj).color)); } - const _regexFromString = /^\$\(([a-z.]+\/)?([a-z-~]+)\)$/i; + const _regexFromString = new RegExp(`^\\$\\((${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?)\\)$`); export function fromString(str: string): ThemeIcon | undefined { const match = _regexFromString.exec(str); if (!match) { return undefined; } - let [, owner, name] = match; - if (!owner || owner === 'codicon/') { - return { id: name }; - } - return { id: owner + name }; + let [, name] = match; + return { id: name }; } export function modify(icon: ThemeIcon, modifier: 'disabled' | 'spin' | undefined): ThemeIcon { @@ -70,58 +67,13 @@ export namespace ThemeIcon { return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id; } - const _regexAsClassName = /^(codicon\/)?([a-z-]+)(~[a-z]+)?$/i; - - export function asClassNameArray(icon: ThemeIcon): string[] { - const match = _regexAsClassName.exec(icon.id); - if (!match) { - return ['codicon', 'codicon-error']; - } - let [, , name, modifier] = match; - // {{SQL CARBON EDIT}} Modifying method to not add 'codicon' in front of sql carbon icons. - let className = ''; - let sqlCarbonIcons = ['book', 'dataExplorer']; - if (sqlCarbonIcons.includes(name)) { - className = name; - } else { - className = `codicon-${name}`; - } - // {{SQL CARBON EDIT}} End of edit - if (modifier) { - return ['codicon', className, modifier.substr(1)]; - } - return ['codicon', className]; - } - - - export function asClassName(icon: ThemeIcon): string { - return asClassNameArray(icon).join(' '); - } - - export function asCSSSelector(icon: ThemeIcon): string { - return '.' + asClassNameArray(icon).join('.'); - } - - export function asCSSIcon(icon: ThemeIcon): CSSIcon { - return { - classNames: asClassName(icon) - }; - } - - export function asCodiconLabel(icon: ThemeIcon): string { - return '$(' + icon.id + ')'; - } - - export function revive(icon: any): ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(icon)) { - return { id: icon.id, color: icon.color ? { id: icon.color.id } : undefined }; - } - return undefined; - } + export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray; + export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName; + export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector; } -export const FileThemeIcon = { id: 'file' }; -export const FolderThemeIcon = { id: 'folder' }; +export const FileThemeIcon = Codicon.file; +export const FolderThemeIcon = Codicon.folder; export function getThemeTypeSelector(type: ColorScheme): string { switch (type) { diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index b7dbab2bf0..8a18520352 100644 --- a/src/vs/platform/theme/test/common/testThemeService.ts +++ b/src/vs/platform/theme/test/common/testThemeService.ts @@ -12,8 +12,11 @@ export class TestColorTheme implements IColorTheme { public readonly label = 'test'; - constructor(private colors: { [id: string]: string; } = {}, public type = ColorScheme.DARK) { - } + constructor( + private colors: { [id: string]: string; } = {}, + public type = ColorScheme.DARK, + public readonly semanticHighlighting = false + ) { } getColor(color: string, useDefault?: boolean): Color | undefined { let value = this.colors[color]; @@ -31,8 +34,6 @@ export class TestColorTheme implements IColorTheme { return undefined; } - readonly semanticHighlighting = false; - get tokenColorMap(): string[] { return []; } diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index d5955ff3c8..7aa8590aab 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -18,6 +18,10 @@ export interface IResourceUndoRedoElement { readonly type: UndoRedoElementType.Resource; readonly resource: URI; readonly label: string; + /** + * Show a message to the user confirming when trying to undo this element + */ + readonly confirmBeforeUndo?: boolean; undo(): Promise | void; redo(): Promise | void; } @@ -26,6 +30,10 @@ export interface IWorkspaceUndoRedoElement { readonly type: UndoRedoElementType.Workspace; readonly resources: readonly URI[]; readonly label: string; + /** + * Show a message to the user confirming when trying to undo this element + */ + readonly confirmBeforeUndo?: boolean; undo(): Promise | void; redo(): Promise | void; diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 00d02820db..918cb4a45d 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -27,6 +27,7 @@ class ResourceStackElement { public readonly type = UndoRedoElementType.Resource; public readonly actual: IUndoRedoElement; public readonly label: string; + public readonly confirmBeforeUndo: boolean; public readonly resourceLabel: string; public readonly strResource: string; @@ -41,6 +42,7 @@ class ResourceStackElement { constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) { this.actual = actual; this.label = actual.label; + this.confirmBeforeUndo = actual.confirmBeforeUndo || false; this.resourceLabel = resourceLabel; this.strResource = strResource; this.resourceLabels = [this.resourceLabel]; @@ -129,6 +131,7 @@ class WorkspaceStackElement { public readonly type = UndoRedoElementType.Workspace; public readonly actual: IWorkspaceUndoRedoElement; public readonly label: string; + public readonly confirmBeforeUndo: boolean; public readonly resourceLabels: string[]; public readonly strResources: string[]; @@ -142,6 +145,7 @@ class WorkspaceStackElement { constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) { this.actual = actual; this.label = actual.label; + this.confirmBeforeUndo = actual.confirmBeforeUndo || false; this.resourceLabels = resourceLabels; this.strResources = strResources; this.groupId = groupId; @@ -811,7 +815,7 @@ export class UndoRedoService implements IUndoRedoService { if (element.canSplit()) { this._splitPastWorkspaceElement(element, ignoreResources); this._notificationService.info(message); - return new WorkspaceVerificationError(this.undo(strResource)); + return new WorkspaceVerificationError(this._undo(strResource, 0, true)); } else { // Cannot safely split this workspace element => flush all undo/redo stacks for (const strResource of element.strResources) { @@ -899,13 +903,13 @@ export class UndoRedoService implements IUndoRedoService { return null; } - private _workspaceUndo(strResource: string, element: WorkspaceStackElement): Promise | void { + private _workspaceUndo(strResource: string, element: WorkspaceStackElement, undoConfirmed: boolean): Promise | void { const affectedEditStacks = this._getAffectedEditStacks(element); const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false); if (verificationError) { return verificationError.returnValue; } - return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks); + return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks, undoConfirmed); } private _isPartOfUndoGroup(element: WorkspaceStackElement): boolean { @@ -933,7 +937,7 @@ export class UndoRedoService implements IUndoRedoService { return false; } - private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise { + private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, undoConfirmed: boolean): Promise { if (element.canSplit() && !this._isPartOfUndoGroup(element)) { // this element can be split @@ -959,7 +963,7 @@ export class UndoRedoService implements IUndoRedoService { if (result.choice === 1) { // choice: undo this file this._splitPastWorkspaceElement(element, null); - return this.undo(strResource); + return this._undo(strResource, 0, true); } // choice: undo in all files @@ -969,6 +973,8 @@ export class UndoRedoService implements IUndoRedoService { if (verificationError1) { return verificationError1.returnValue; } + + undoConfirmed = true; } // prepare @@ -989,10 +995,10 @@ export class UndoRedoService implements IUndoRedoService { for (const editStack of editStackSnapshot.editStacks) { editStack.moveBackward(element); } - return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId)); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed)); } - private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement, undoConfirmed: boolean): Promise | void { if (!element.isValid) { // invalid element => immediately flush edit stack! editStack.flushAllElements(); @@ -1008,7 +1014,7 @@ export class UndoRedoService implements IUndoRedoService { } return this._invokeResourcePrepare(element, (cleanup) => { editStack.moveBackward(element); - return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId)); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed)); }); } @@ -1037,23 +1043,29 @@ export class UndoRedoService implements IUndoRedoService { return [matchedElement, matchedStrResource]; } - private _continueUndoInGroup(groupId: number): Promise | void { + private _continueUndoInGroup(groupId: number, undoConfirmed: boolean): Promise | void { if (!groupId) { return; } const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId); if (matchedStrResource) { - return this.undo(matchedStrResource); + return this._undo(matchedStrResource, 0, undoConfirmed); } } - public undo(resourceOrSource: URI | UndoRedoSource | string): Promise | void { + public undo(resourceOrSource: URI | UndoRedoSource): Promise | void { if (resourceOrSource instanceof UndoRedoSource) { const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id); - return matchedStrResource ? this.undo(matchedStrResource) : undefined; + return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id, false) : undefined; } - const strResource = typeof resourceOrSource === 'string' ? resourceOrSource : this.getUriComparisonKey(resourceOrSource); + if (typeof resourceOrSource === 'string') { + return this._undo(resourceOrSource, 0, false); + } + return this._undo(this.getUriComparisonKey(resourceOrSource), 0, false); + } + + private _undo(strResource: string, sourceId: number = 0, undoConfirmed: boolean): Promise | void { if (!this._editStacks.has(strResource)) { return; } @@ -1069,15 +1081,21 @@ export class UndoRedoService implements IUndoRedoService { const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId); if (element !== matchedElement && matchedStrResource) { // there is an element in the same group that should be undone before this one - return this.undo(matchedStrResource); + return this._undo(matchedStrResource, sourceId, undoConfirmed); } } + const shouldPromptForConfirmation = (element.sourceId !== sourceId || element.confirmBeforeUndo); + if (shouldPromptForConfirmation && !undoConfirmed) { + // Hit a different source or the element asks for prompt before undo, prompt for confirmation + return this._confirmAndContinueUndo(strResource, sourceId, element); + } + try { if (element.type === UndoRedoElementType.Workspace) { - return this._workspaceUndo(strResource, element); + return this._workspaceUndo(strResource, element, undoConfirmed); } else { - return this._resourceUndo(editStack, element); + return this._resourceUndo(editStack, element, undoConfirmed); } } finally { if (DEBUG) { @@ -1086,6 +1104,28 @@ export class UndoRedoService implements IUndoRedoService { } } + private async _confirmAndContinueUndo(strResource: string, sourceId: number, element: StackElement): Promise { + const result = await this._dialogService.show( + Severity.Info, + nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label), + [ + nls.localize('confirmDifferentSource.ok', "Undo"), + nls.localize('cancel', "Cancel"), + ], + { + cancelId: 1 + } + ); + + if (result.choice === 1) { + // choice: cancel + return; + } + + // choice: undo + return this._undo(strResource, sourceId, true); + } + private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] { if (!sourceId) { return [null, null]; @@ -1128,7 +1168,7 @@ export class UndoRedoService implements IUndoRedoService { if (element.canSplit()) { this._splitFutureWorkspaceElement(element, ignoreResources); this._notificationService.info(message); - return new WorkspaceVerificationError(this.redo(strResource)); + return new WorkspaceVerificationError(this._redo(strResource)); } else { // Cannot safely split this workspace element => flush all undo/redo stacks for (const strResource of element.strResources) { @@ -1300,16 +1340,22 @@ export class UndoRedoService implements IUndoRedoService { const [, matchedStrResource] = this._findClosestRedoElementInGroup(groupId); if (matchedStrResource) { - return this.redo(matchedStrResource); + return this._redo(matchedStrResource); } } public redo(resourceOrSource: URI | UndoRedoSource | string): Promise | void { if (resourceOrSource instanceof UndoRedoSource) { const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id); - return matchedStrResource ? this.redo(matchedStrResource) : undefined; + return matchedStrResource ? this._redo(matchedStrResource) : undefined; } - const strResource = typeof resourceOrSource === 'string' ? resourceOrSource : this.getUriComparisonKey(resourceOrSource); + if (typeof resourceOrSource === 'string') { + return this._redo(resourceOrSource); + } + return this._redo(this.getUriComparisonKey(resourceOrSource)); + } + + private _redo(strResource: string): Promise | void { if (!this._editStacks.has(strResource)) { return; } @@ -1325,7 +1371,7 @@ export class UndoRedoService implements IUndoRedoService { const [matchedElement, matchedStrResource] = this._findClosestRedoElementInGroup(element.groupId); if (element !== matchedElement && matchedStrResource) { // there is an element in the same group that should be redone before this one - return this.redo(matchedStrResource); + return this._redo(matchedStrResource); } } diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index d83f84e42b..4fa21cf4ef 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -23,9 +23,9 @@ suite('UndoRedoService', () => { const resource = URI.file('test.txt'); const service = createUndoRedoService(); - assert.equal(service.canUndo(resource), false); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), false); + assert.strictEqual(service.canUndo(resource), false); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), false); assert.ok(service.getLastElement(resource) === null); let undoCall1 = 0; @@ -39,27 +39,27 @@ suite('UndoRedoService', () => { }; service.pushElement(element1); - assert.equal(undoCall1, 0); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 0); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element1); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource), false); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource), false); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); service.redo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element1); let undoCall2 = 0; @@ -73,24 +73,24 @@ suite('UndoRedoService', () => { }; service.pushElement(element2); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 0); - assert.equal(redoCall2, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 0); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element2); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); let undoCall3 = 0; @@ -104,28 +104,28 @@ suite('UndoRedoService', () => { }; service.pushElement(element3); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(undoCall3, 0); - assert.equal(redoCall3, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), false); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(undoCall3, 0); + assert.strictEqual(redoCall3, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), false); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === element3); service.undo(resource); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall2, 1); - assert.equal(redoCall2, 0); - assert.equal(undoCall3, 1); - assert.equal(redoCall3, 0); - assert.equal(service.canUndo(resource), true); - assert.equal(service.canRedo(resource), true); - assert.equal(service.hasElements(resource), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall2, 1); + assert.strictEqual(redoCall2, 0); + assert.strictEqual(undoCall3, 1); + assert.strictEqual(redoCall3, 0); + assert.strictEqual(service.canUndo(resource), true); + assert.strictEqual(service.canRedo(resource), true); + assert.strictEqual(service.hasElements(resource), true); assert.ok(service.getLastElement(resource) === null); }); @@ -169,50 +169,50 @@ suite('UndoRedoService', () => { }; service.pushElement(element1); - assert.equal(service.canUndo(resource1), true); - assert.equal(service.canRedo(resource1), false); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(service.canUndo(resource1), true); + assert.strictEqual(service.canRedo(resource1), false); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === element1); - assert.equal(service.canUndo(resource2), true); - assert.equal(service.canRedo(resource2), false); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), true); + assert.strictEqual(service.canRedo(resource2), false); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === element1); await service.undo(resource1); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 0); - assert.equal(service.canUndo(resource1), false); - assert.equal(service.canRedo(resource1), true); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 0); + assert.strictEqual(service.canUndo(resource1), false); + assert.strictEqual(service.canRedo(resource1), true); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === null); - assert.equal(service.canUndo(resource2), false); - assert.equal(service.canRedo(resource2), true); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), false); + assert.strictEqual(service.canRedo(resource2), true); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === null); await service.redo(resource2); - assert.equal(undoCall1, 1); - assert.equal(redoCall1, 1); - assert.equal(undoCall11, 0); - assert.equal(redoCall11, 0); - assert.equal(undoCall12, 0); - assert.equal(redoCall12, 0); - assert.equal(service.canUndo(resource1), true); - assert.equal(service.canRedo(resource1), false); - assert.equal(service.hasElements(resource1), true); + assert.strictEqual(undoCall1, 1); + assert.strictEqual(redoCall1, 1); + assert.strictEqual(undoCall11, 0); + assert.strictEqual(redoCall11, 0); + assert.strictEqual(undoCall12, 0); + assert.strictEqual(redoCall12, 0); + assert.strictEqual(service.canUndo(resource1), true); + assert.strictEqual(service.canRedo(resource1), false); + assert.strictEqual(service.hasElements(resource1), true); assert.ok(service.getLastElement(resource1) === element1); - assert.equal(service.canUndo(resource2), true); - assert.equal(service.canRedo(resource2), false); - assert.equal(service.hasElements(resource2), true); + assert.strictEqual(service.canUndo(resource2), true); + assert.strictEqual(service.canRedo(resource2), false); + assert.strictEqual(service.hasElements(resource2), true); assert.ok(service.getLastElement(resource2) === element1); }); test('UndoRedoGroup.None uses id 0', () => { - assert.equal(UndoRedoGroup.None.id, 0); - assert.equal(UndoRedoGroup.None.nextOrder(), 0); - assert.equal(UndoRedoGroup.None.nextOrder(), 0); + assert.strictEqual(UndoRedoGroup.None.id, 0); + assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0); + assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0); }); }); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index b867d5560d..3b37a2f1dd 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -15,6 +15,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e import { ILogService } from 'vs/platform/log/common/log'; import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService'; import { IRequestService } from 'vs/platform/request/common/request'; +import product from 'vs/platform/product/common/product'; export class DarwinUpdateService extends AbstractUpdateService { @@ -56,7 +57,12 @@ export class DarwinUpdateService extends AbstractUpdateService { } protected buildUpdateFeedUrl(quality: string): string | undefined { - const assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; + let assetID: string; + if (!product.darwinUniversalAssetId) { + assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; + } else { + assetID = product.darwinUniversalAssetId; + } const url = createUpdateURL(assetID, quality); try { electron.autoUpdater.setFeedURL({ url }); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 0359cd6f98..f538314de0 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -55,9 +55,8 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { - // {{SQL CARBON EDIT}} const result = path.join(tmpdir(), `sqlops-update-${product.target}-${process.arch}`); - return pfs.mkdirp(result, undefined).then(() => result); + return fs.promises.mkdir(result, { recursive: true }).then(() => result); } constructor( @@ -147,7 +146,7 @@ export class Win32UpdateService extends AbstractUpdateService { return this.requestService.request({ url }, CancellationToken.None) .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) .then(hash ? () => checksum(downloadPath, update.hash) : () => undefined) - .then(() => pfs.rename(downloadPath, updatePackagePath)) + .then(() => fs.promises.rename(downloadPath, updatePackagePath)) .then(() => updatePackagePath); }); }).then(packagePath => { @@ -198,7 +197,7 @@ export class Win32UpdateService extends AbstractUpdateService { const promises = versions.filter(filter).map(async one => { try { - await pfs.unlink(path.join(cachePath, one)); + await fs.promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } diff --git a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts index a9fa638076..2459555630 100644 --- a/src/vs/platform/userDataSync/common/extensionsStorageSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsStorageSync.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -32,7 +33,7 @@ export class ExtensionsStorageSyncService extends Disposable implements IExtensi declare readonly _serviceBrand: undefined; private static toKey(extension: IExtensionIdWithVersion): string { - return `extensionKeys/${extension.id}@${extension.version}`; + return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`; } private static fromKey(key: string): IExtensionIdWithVersion | undefined { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 0ffdb33fe3..840299efbd 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; @@ -25,8 +25,9 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { CancellationToken } from 'vs/base/common/cancellation'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { getErrorMessage } from 'vs/base/common/errors'; -import { forEach, IStringDictionary } from 'vs/base/common/collections'; +import { IStringDictionary } from 'vs/base/common/collections'; import { IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { Promises } from 'vs/base/common/async'; interface IExtensionResourceMergeResult extends IAcceptResult { readonly added: ISyncExtension[]; @@ -73,6 +74,15 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen return extensions; } +function getExtensionStorageState(publisher: string, name: string, storageService: IStorageService): IStringDictionary { + const extensionStorageValue = storageService.get(getExtensionId(publisher, name) /* use the same id used in extension host */, StorageScope.GLOBAL) || '{}'; + return JSON.parse(extensionStorageValue); +} + +function storeExtensionStorageState(publisher: string, name: string, extensionState: IStringDictionary, storageService: IStorageService): void { + storageService.store(getExtensionId(publisher, name) /* use the same id used in extension host */, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); +} + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); @@ -99,7 +109,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, - @IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService, + @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService configurationService: IConfigurationService, @@ -125,7 +135,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); if (remoteExtensions) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); @@ -209,7 +219,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions); const { added, removed, updated, remote } = mergeResult; return { @@ -225,7 +235,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; if (remoteExtensions !== null) { const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions); @@ -285,7 +295,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async resolveContent(uri: URI): Promise { if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { const installedExtensions = await this.extensionManagementService.getInstalled(); - const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions); + const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier))); return this.format(localExtensions); } @@ -348,7 +358,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (removed.length) { const extensionsToRemove = installedExtensions.filter(({ identifier, isBuiltin }) => !isBuiltin && removed.some(r => areSameExtensions(identifier, r))); - await Promise.all(extensionsToRemove.map(async extensionToRemove => { + await Promises.settled(extensionsToRemove.map(async extensionToRemove => { this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id); await this.extensionManagementService.uninstall(extensionToRemove, { donotIncludePack: true, donotCheckDependents: true }); this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id); @@ -357,13 +367,13 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } if (added.length || updated.length) { - await Promise.all([...added, ...updated].map(async e => { + await Promises.settled([...added, ...updated].map(async e => { const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); // Builtin Extension Sync: Enablement & State if (installedExtension && installedExtension.isBuiltin) { if (e.state && installedExtension.manifest.version === e.version) { - this.updateExtensionState(e.state, e.identifier.id, installedExtension.manifest.version); + this.updateExtensionState(e.state, installedExtension.manifest.publisher, installedExtension.manifest.name, installedExtension.manifest.version); } if (e.disabled) { this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id); @@ -382,14 +392,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier); /* Update extension state only if - * extension is installed and version is same as synced version or - * extension is not installed and installable + * extension is installed and version is same as synced version or + * extension is not installed and installable */ if (e.state && (installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */ : !!extension /* Installable */) ) { - this.updateExtensionState(e.state, e.identifier.id, installedExtension?.manifest.version); + const publisher = installedExtension ? installedExtension.manifest.publisher : extension!.publisher; + const name = installedExtension ? installedExtension.manifest.name : extension!.name; + this.updateExtensionState(e.state, publisher, name, installedExtension?.manifest.version); } if (extension) { @@ -436,15 +448,15 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private updateExtensionState(state: IStringDictionary, id: string, version?: string): void { - const extensionState = JSON.parse(this.storageService.get(id, StorageScope.GLOBAL) || '{}'); - const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id, version }) : undefined; + private updateExtensionState(state: IStringDictionary, publisher: string, name: string, version: string | undefined): void { + const extensionState = getExtensionStorageState(publisher, name, this.storageService); + const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id: getGalleryExtensionId(publisher, name), version }) : undefined; if (keys) { - keys.forEach(key => extensionState[key] = state[key]); + keys.forEach(key => { extensionState[key] = state[key]; }); } else { - forEach(state, ({ key, value }) => extensionState[key] = value); + Object.keys(state).forEach(key => extensionState[key] = state[key]); } - this.storageService.store(id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); + storeExtensionStorageState(publisher, name, extensionState, this.storageService); } private parseExtensions(syncData: ISyncData): ISyncExtension[] { @@ -465,8 +477,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse try { const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version }); if (keys) { - const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}'; - const extensionStorageState = JSON.parse(extensionStorageValue); + const extensionStorageState = getExtensionStorageState(manifest.publisher, manifest.name, this.storageService); syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary, key) => { if (keys.includes(key)) { state[key] = extensionStorageState[key]; @@ -490,6 +501,7 @@ export class ExtensionsInitializer extends AbstractInitializer { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, @IStorageService private readonly storageService: IStorageService, + @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -511,12 +523,18 @@ export class ExtensionsInitializer extends AbstractInitializer { const newlyEnabledExtensions: ILocalExtension[] = []; const installedExtensions = await this.extensionManagementService.getInstalled(); const newExtensionsToSync = new Map(); - const installedExtensionsToSync: ISyncExtension[] = []; + const installedExtensionsToSync: { syncExtension: ISyncExtension, installedExtension: ILocalExtension }[] = []; const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] }; const toDisable: IExtensionIdentifier[] = []; for (const extension of remoteExtensions) { - if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) { - installedExtensionsToSync.push(extension); + if (this.ignoredExtensionsManagementService.hasToNeverSyncExtension(extension.identifier.id)) { + // Skip extension ignored to sync + continue; + } + + const installedExtension = installedExtensions.find(i => areSameExtensions(i.identifier, extension.identifier)); + if (installedExtension) { + installedExtensionsToSync.push({ syncExtension: extension, installedExtension }); if (extension.disabled) { toDisable.push(extension.identifier); } @@ -528,17 +546,39 @@ export class ExtensionsInitializer extends AbstractInitializer { } else { toInstall.names.push(extension.identifier.id); } + if (extension.disabled) { + toDisable.push(extension.identifier); + } } } } + // 1. Initialise already installed extensions state + for (const { syncExtension, installedExtension } of installedExtensionsToSync) { + if (syncExtension.state) { + const extensionState = getExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, this.storageService); + Object.keys(syncExtension.state).forEach(key => extensionState[key] = syncExtension.state![key]); + storeExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, extensionState, this.storageService); + } + } + + // 2. Initialise extensions enablement + if (toDisable.length) { + for (const identifier of toDisable) { + this.logService.trace(`Disabling extension...`, identifier.id); + await this.extensionEnablementService.disableExtension(identifier); + this.logService.info(`Disabling extension`, identifier.id); + } + } + + // 3. Install extensions if (toInstall.names.length || toInstall.uuids.length) { const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage; for (const galleryExtension of galleryExtensions) { try { const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!; if (extensionToSync.state) { - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL, StorageTarget.MACHINE); + storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService); } this.logService.trace(`Installing extension...`, galleryExtension.identifier.id); const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */); @@ -552,22 +592,6 @@ export class ExtensionsInitializer extends AbstractInitializer { } } - if (toDisable.length) { - for (const identifier of toDisable) { - this.logService.trace(`Enabling extension...`, identifier.id); - await this.extensionEnablementService.disableExtension(identifier); - this.logService.info(`Enabled extension`, identifier.id); - } - } - - for (const extensionToSync of installedExtensionsToSync) { - if (extensionToSync.state) { - const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}'); - forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value); - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); - } - } - return newlyEnabledExtensions; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 74b42b8a98..424c084c01 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -31,9 +31,10 @@ export function getDisallowedIgnoredSettings(): string[] { export function getDefaultIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync); const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); const disallowedSettings = getDisallowedIgnoredSettings(); - return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]); + return distinct([CONFIGURATION_SYNC_STORE_KEY, ...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]); } export function registerConfiguration(): IDisposable { @@ -52,10 +53,6 @@ export function registerConfiguration(): IDisposable { scope: ConfigurationScope.APPLICATION, tags: ['sync', 'usesOnlineServices'] }, - 'sync.keybindingsPerPlatform': { - type: 'boolean', - deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"), - }, 'settingsSync.ignoredExtensions': { 'type': 'array', markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."), @@ -70,10 +67,6 @@ export function registerConfiguration(): IDisposable { disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] }, - 'sync.ignoredExtensions': { - 'type': 'array', - deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"), - }, 'settingsSync.ignoredSettings': { 'type': 'array', description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."), @@ -84,10 +77,6 @@ export function registerConfiguration(): IDisposable { uniqueItems: true, disallowSyncIgnore: true, tags: ['sync', 'usesOnlineServices'] - }, - 'sync.ignoredSettings': { - 'type': 'array', - deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"), } } }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts index 16a25c56c5..0990fb5606 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts @@ -11,6 +11,7 @@ import { IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { toLocalISOString } from 'vs/base/common/date'; import { VSBuffer } from 'vs/base/common/buffer'; +import { Promises } from 'vs/base/common/async'; export class UserDataSyncBackupStoreService extends Disposable implements IUserDataSyncBackupStoreService { @@ -86,9 +87,9 @@ export class UserDataSyncBackupStoreService extends Disposable implements IUserD if (remaining < 10) { toDelete = toDelete.slice(10 - remaining); } - await Promise.all(toDelete.map(stat => { + await Promises.settled(toDelete.map(async stat => { this.logService.info('Deleting from backup', stat.resource.path); - this.fileService.del(stat.resource); + await this.fileService.del(stat.resource); })); } } catch (e) { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 3cff0f3845..a2c7c2995d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -445,131 +445,171 @@ class ManualSyncTask extends Disposable implements IManualSyncTask { } async preview(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (this.isDisposed) { - throw new Error('Disposed'); + try { + if (this.isDisposed) { + throw new Error('Disposed'); + } + if (!this.previewsPromise) { + this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); + } + if (!this.previews) { + this.previews = await this.previewsPromise; + } + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - if (!this.previewsPromise) { - this.previewsPromise = createCancelablePromise(token => this.getPreviews(token)); - } - if (!this.previews) { - this.previews = await this.previewsPromise; - } - return this.previews; } async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); + try { + return await this.performAction(resource, sychronizer => sychronizer.accept(resource, content)); + } catch (error) { + this.logService.error(error); + throw error; + } } async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - if (resource) { - return this.performAction(resource, sychronizer => sychronizer.merge(resource)); - } else { - return this.mergeAll(); + try { + if (resource) { + return await this.performAction(resource, sychronizer => sychronizer.merge(resource)); + } else { + return await this.mergeAll(); + } + } catch (error) { + this.logService.error(error); + throw error; } } async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> { - return this.performAction(resource, sychronizer => sychronizer.discard(resource)); + try { + return await this.performAction(resource, sychronizer => sychronizer.discard(resource)); + } catch (error) { + this.logService.error(error); + throw error; + } } async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('Missing preview. Create preview and try again.'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot discard while synchronizing resources'); - } + try { + if (!this.previews) { + throw new Error('Missing preview. Create preview and try again.'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot discard while synchronizing resources'); + } - const conflictResources: URI[] = []; - for (const [, syncResourcePreview] of this.previews) { - for (const resourcePreview of syncResourcePreview.resourcePreviews) { - if (resourcePreview.mergeState === MergeState.Conflict) { - conflictResources.push(resourcePreview.previewResource); + const conflictResources: URI[] = []; + for (const [, syncResourcePreview] of this.previews) { + for (const resourcePreview of syncResourcePreview.resourcePreviews) { + if (resourcePreview.mergeState === MergeState.Conflict) { + conflictResources.push(resourcePreview.previewResource); + } } } - } - for (const resource of conflictResources) { - await this.discard(resource); + for (const resource of conflictResources) { + await this.discard(resource); + } + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - return this.previews; } async apply(): Promise<[SyncResource, ISyncResourcePreview][]> { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - const previews: [SyncResource, ISyncResourcePreview][] = []; - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); + } + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + const previews: [SyncResource, ISyncResourcePreview][] = []; + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - /* merge those which are not yet merged */ - for (const resourcePreview of preview.resourcePreviews) { - if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { - await synchroniser.merge(resourcePreview.previewResource); + /* merge those which are not yet merged */ + for (const resourcePreview of preview.resourcePreviews) { + if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) { + await synchroniser.merge(resourcePreview.previewResource); + } } - } - /* apply */ - const newPreview = await synchroniser.apply(false, this.syncHeaders); - if (newPreview) { - previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); - } + /* apply */ + const newPreview = await synchroniser.apply(false, this.syncHeaders); + if (newPreview) { + previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview)); + } - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = previews; + return this.previews; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = previews; - return this.previews; } async pull(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - await synchroniser.accept(resourcePreview.remoteResource); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.remoteResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = []; } async push(): Promise { - if (!this.previews) { - throw new Error('You need to create preview before applying'); - } - if (this.synchronizingResources.length) { - throw new Error('Cannot pull while synchronizing resources'); - } - for (const [syncResource, preview] of this.previews) { - this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); - this._onSynchronizeResources.fire(this.synchronizingResources); - const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; - for (const resourcePreview of preview.resourcePreviews) { - await synchroniser.accept(resourcePreview.localResource); + try { + if (!this.previews) { + throw new Error('You need to create preview before applying'); } - await synchroniser.apply(true, this.syncHeaders); - this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); - this._onSynchronizeResources.fire(this.synchronizingResources); + if (this.synchronizingResources.length) { + throw new Error('Cannot pull while synchronizing resources'); + } + for (const [syncResource, preview] of this.previews) { + this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]); + this._onSynchronizeResources.fire(this.synchronizingResources); + const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!; + for (const resourcePreview of preview.resourcePreviews) { + await synchroniser.accept(resourcePreview.localResource); + } + await synchroniser.apply(true, this.syncHeaders); + this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1); + this._onSynchronizeResources.fire(this.synchronizingResources); + } + this.previews = []; + } catch (error) { + this.logService.error(error); + throw error; } - this.previews = []; } async stop(): Promise { diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index efeecb065d..38a99ade99 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable, } from 'vs/base/common/lifecycle'; import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; @@ -180,6 +180,12 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync /* A requests session that limits requests per sessions */ this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService, this.logService); this.initDonotMakeRequestsUntil(); + this._register(toDisposable(() => { + if (this.resetDonotMakeRequestsUntilPromise) { + this.resetDonotMakeRequestsUntilPromise.cancel(); + this.resetDonotMakeRequestsUntilPromise = undefined; + } + })); } setAuthToken(token: string, type: string): void { @@ -210,6 +216,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync if (this._donotMakeRequestsUntil) { this.storageService.store(DONOT_MAKE_REQUESTS_UNTIL_KEY, this._donotMakeRequestsUntil.getTime(), StorageScope.GLOBAL, StorageTarget.MACHINE); this.resetDonotMakeRequestsUntilPromise = createCancelablePromise(token => timeout(this._donotMakeRequestsUntil!.getTime() - Date.now(), token).then(() => this.setDonotMakeRequestsUntil(undefined))); + this.resetDonotMakeRequestsUntilPromise.then(null, e => null /* ignore error */); } else { this.storageService.remove(DONOT_MAKE_REQUESTS_UNTIL_KEY, StorageScope.GLOBAL); } diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 1c84137837..4c1d3f605d 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -181,7 +181,7 @@ suite('TestSynchronizer - Auto Sync', () => { teardown(() => disposableStore.clear()); test('status is syncing', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -198,7 +198,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set correctly when sync is finished', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); const actual: SyncStatus[] = []; @@ -210,7 +210,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set correctly when sync has errors', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasError: true, hasConflicts: false }; testObject.syncBarrier.open(); @@ -227,7 +227,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is set to hasConflicts when asked to sync if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -238,7 +238,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if syncing already', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); const promise = Event.toPromise(testObject.onDoSyncCall.event); testObject.sync(await client.manifest()); @@ -255,7 +255,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if disabled', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); client.instantiationService.get(IUserDataSyncResourceEnablementService).setResourceEnablement(testObject.resource, false); const actual: SyncStatus[] = []; @@ -268,7 +268,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('sync should not run if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -282,7 +282,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept preview during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -300,7 +300,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept remote during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -323,7 +323,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept local during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -345,7 +345,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept new content during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -368,7 +368,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept delete during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -390,7 +390,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept deleted local during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); const fileService = client.instantiationService.get(IFileService); @@ -411,7 +411,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('accept deleted remote during conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); const fileService = client.instantiationService.get(IFileService); await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content')); @@ -431,7 +431,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('request latest data on precondition failure', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); // Sync once testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -458,7 +458,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('no requests are made to server when local change is triggered', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -471,7 +471,7 @@ suite('TestSynchronizer - Auto Sync', () => { }); test('status is reset when getting latest remote data fails', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.failWhenGettingLatestRemoteUserData = true; try { @@ -502,7 +502,7 @@ suite('TestSynchronizer - Manual Sync', () => { teardown(() => disposableStore.clear()); test('preview', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -514,7 +514,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -528,7 +528,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -542,7 +542,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -557,7 +557,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -577,7 +577,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -597,7 +597,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> merge -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -617,7 +617,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -630,7 +630,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -650,7 +650,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -665,7 +665,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -681,7 +681,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -696,7 +696,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -712,7 +712,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -728,7 +728,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -744,7 +744,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> merge -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -764,7 +764,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -785,7 +785,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('preivew -> accept -> discard -> merge -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -808,7 +808,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -820,7 +820,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -834,7 +834,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -849,7 +849,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -864,7 +864,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> merge -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -887,7 +887,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -901,7 +901,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preview -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -923,7 +923,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -938,7 +938,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -954,7 +954,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -969,7 +969,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> accept', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -985,7 +985,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -1001,7 +1001,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> merge', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: true, hasError: false }; testObject.syncBarrier.open(); @@ -1017,7 +1017,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> accept -> discard', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); @@ -1033,7 +1033,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); @@ -1053,7 +1053,7 @@ suite('TestSynchronizer - Manual Sync', () => { }); test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + const testObject: TestSynchroniser = disposableStore.add(client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings)); testObject.syncResult = { hasConflicts: false, hasError: false }; testObject.syncBarrier.open(); await testObject.sync(await client.manifest()); diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 1b60685662..c0f6e98a8a 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -40,7 +40,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change await testObject.triggerSync([SyncResource.Settings], false, false); @@ -62,7 +62,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { @@ -88,7 +88,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus once await testObject.triggerSync(['windowFocus'], true, false); @@ -110,7 +110,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run(); target.reset(); - const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { @@ -129,7 +129,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); @@ -165,7 +165,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -185,7 +185,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -220,7 +220,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is const target = new UserDataSyncTestServer(); const client = disposableStore.add(new UserDataSyncClient(target)); await client.setUp(); - const testObject: TestUserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); // Sync once and reset requests await testObject.sync(); @@ -250,7 +250,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Reset from the first client @@ -279,7 +279,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Disable current machine @@ -311,7 +311,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Remove current machine @@ -339,7 +339,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.sync(); // Reset from the first client @@ -371,7 +371,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); const errorPromise = Event.toPromise(testObject.onError); while (target.requests.length < 5) { @@ -389,7 +389,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); while (target.requests.length < 5) { await testObject.sync(); @@ -407,7 +407,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.triggerSync(['some reason'], true, true); assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); @@ -419,7 +419,7 @@ suite.skip('UserDataAutoSyncService', () => { // {{SQL CARBON EDIT}} Service is // Set up and sync from the test client const testClient = disposableStore.add(new UserDataSyncClient(target)); await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); await testObject.triggerSync(['some reason'], true, false); assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 61e9628c0f..4b7b2be0a9 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -83,9 +83,9 @@ export class UserDataSyncClient extends Disposable { fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); this.instantiationService.stub(IFileService, fileService); - this.instantiationService.stub(IStorageService, new InMemoryStorageService()); + this.instantiationService.stub(IStorageService, this._register(new InMemoryStorageService())); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); @@ -93,20 +93,20 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); - this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService)); - this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncStoreManagementService, this._register(this.instantiationService.createInstance(UserDataSyncStoreManagementService))); + this.instantiationService.stub(IUserDataSyncStoreService, this._register(this.instantiationService.createInstance(UserDataSyncStoreService))); - const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService); + const userDataSyncAccountService: IUserDataSyncAccountService = this._register(this.instantiationService.createInstance(UserDataSyncAccountService)); await userDataSyncAccountService.updateAccount({ authenticationProviderId: 'authenticationProviderId', token: 'token' }); this.instantiationService.stub(IUserDataSyncAccountService, userDataSyncAccountService); - this.instantiationService.stub(IUserDataSyncMachinesService, this.instantiationService.createInstance(UserDataSyncMachinesService)); - this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService)); + this.instantiationService.stub(IUserDataSyncMachinesService, this._register(this.instantiationService.createInstance(UserDataSyncMachinesService))); + this.instantiationService.stub(IUserDataSyncBackupStoreService, this._register(this.instantiationService.createInstance(UserDataSyncBackupStoreService))); this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); - this.instantiationService.stub(IUserDataSyncResourceEnablementService, this.instantiationService.createInstance(UserDataSyncResourceEnablementService)); + this.instantiationService.stub(IUserDataSyncResourceEnablementService, this._register(this.instantiationService.createInstance(UserDataSyncResourceEnablementService))); - this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); - this.instantiationService.stub(IExtensionsStorageSyncService, this.instantiationService.createInstance(ExtensionsStorageSyncService)); + this.instantiationService.stub(IGlobalExtensionEnablementService, this._register(this.instantiationService.createInstance(GlobalExtensionEnablementService))); + this.instantiationService.stub(IExtensionsStorageSyncService, this._register(this.instantiationService.createInstance(ExtensionsStorageSyncService))); this.instantiationService.stub(IIgnoredExtensionsManagementService, this.instantiationService.createInstance(IgnoredExtensionsManagementService)); this.instantiationService.stub(IExtensionManagementService, >{ async getInstalled() { return []; }, @@ -118,8 +118,8 @@ export class UserDataSyncClient extends Disposable { async getCompatibleExtension() { return null; } }); - this.instantiationService.stub(IUserDataAutoSyncEnablementService, this.instantiationService.createInstance(UserDataAutoSyncEnablementService)); - this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); + this.instantiationService.stub(IUserDataAutoSyncEnablementService, this._register(this.instantiationService.createInstance(UserDataAutoSyncEnablementService))); + this.instantiationService.stub(IUserDataSyncService, this._register(this.instantiationService.createInstance(UserDataSyncService))); if (!empty) { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index 13ac7d66e3..492c03c653 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -58,7 +58,7 @@ suite('UserDataSyncStoreManagementService', () => { authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }] }; - const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService); + const testObject: IUserDataSyncStoreManagementService = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreManagementService)); assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString()); assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString()); @@ -419,7 +419,7 @@ suite('UserDataSyncStoreService', () => { await testObject.manifest(); } catch (e) { } - const target = client.instantiationService.createInstance(UserDataSyncStoreService); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); assert.equal(target.donotMakeRequestsUntil?.getTime(), testObject.donotMakeRequestsUntil?.getTime()); }); @@ -434,7 +434,7 @@ suite('UserDataSyncStoreService', () => { } catch (e) { } await timeout(300); - const target = client.instantiationService.createInstance(UserDataSyncStoreService); + const target = disposableStore.add(client.instantiationService.createInstance(UserDataSyncStoreService)); assert.ok(!target.donotMakeRequestsUntil); }); diff --git a/src/vs/platform/webview/common/resourceLoader.ts b/src/vs/platform/webview/common/resourceLoader.ts index 899764dbae..657084a289 100644 --- a/src/vs/platform/webview/common/resourceLoader.ts +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -9,6 +9,9 @@ import { isUNC } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; import { sep } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; +import { IHeaders } from 'vs/base/parts/request/common/request'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes'; @@ -17,39 +20,99 @@ import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes' export const webviewPartitionId = 'webview'; export namespace WebviewResourceResponse { - export enum Type { Success, Failed, AccessDenied } + export enum Type { Success, Failed, AccessDenied, NotModified } export class StreamSuccess { readonly type = Type.Success; constructor( public readonly stream: VSBufferReadableStream, - public readonly mimeType: string + public readonly etag: string | undefined, + public readonly mimeType: string, ) { } } export const Failed = { type: Type.Failed } as const; export const AccessDenied = { type: Type.AccessDenied } as const; - export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied; + export class NotModified { + readonly type = Type.NotModified; + + constructor( + public readonly mimeType: string, + ) { } + } + + export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied | NotModified; } -interface FileReader { - readFileStream(resource: URI): Promise; +export namespace WebviewFileReadResponse { + export enum Type { Success, NotModified } + + export class StreamSuccess { + readonly type = Type.Success; + + constructor( + public readonly stream: VSBufferReadableStream, + public readonly etag: string | undefined + ) { } + } + + export const NotModified = { type: Type.NotModified } as const; + + export type Response = StreamSuccess | typeof NotModified; +} + +/** + * Wraps a call to `IFileService.readFileStream` and converts the result to a `WebviewFileReadResponse.Response` + */ +export async function readFileStream( + fileService: IFileService, + resource: URI, + etag: string | undefined, +): Promise { + try { + const result = await fileService.readFileStream(resource, { etag }); + return new WebviewFileReadResponse.StreamSuccess(result.value, result.etag); + } catch (e) { + if (e instanceof FileOperationError) { + const result = e.fileOperationResult; + + // NotModified status is expected and can be handled gracefully + if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + return WebviewFileReadResponse.NotModified; + } + } + + // Otherwise the error is unexpected. Re-throw and let caller handle it + throw e; + } +} + +export interface WebviewResourceFileReader { + readFileStream(resource: URI, etag: string | undefined): Promise; } export async function loadLocalResource( requestUri: URI, + ifNoneMatch: string | undefined, options: { extensionLocation: URI | undefined; roots: ReadonlyArray; remoteConnectionData?: IRemoteConnectionData | null; rewriteUri?: (uri: URI) => URI, }, - fileReader: FileReader, + fileReader: WebviewResourceFileReader, requestService: IRequestService, + logService: ILogService, + token: CancellationToken, ): Promise { + logService.debug(`loadLocalResource - being. requestUri=${requestUri}`); + let resourceToLoad = getResourceToLoad(requestUri, options.roots); + + logService.debug(`loadLocalResource - found resource to load. requestUri=${requestUri}, resourceToLoad=${resourceToLoad}`); + if (!resourceToLoad) { return WebviewResourceResponse.AccessDenied; } @@ -62,18 +125,43 @@ export async function loadLocalResource( } if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) { - const response = await requestService.request({ url: resourceToLoad.toString(true) }, CancellationToken.None); + const headers: IHeaders = {}; + if (ifNoneMatch) { + headers['If-None-Match'] = ifNoneMatch; + } + + const response = await requestService.request({ + url: resourceToLoad.toString(true), + headers: headers + }, token); + + logService.debug(`loadLocalResource - Loaded over http(s). requestUri=${requestUri}, response=${response.res.statusCode}`); + if (response.res.statusCode === 200) { - return new WebviewResourceResponse.StreamSuccess(response.stream, mime); + return new WebviewResourceResponse.StreamSuccess(response.stream, response.res.headers['etag'], mime); } return WebviewResourceResponse.Failed; } try { - const contents = await fileReader.readFileStream(resourceToLoad); - return new WebviewResourceResponse.StreamSuccess(contents, mime); + const contents = await fileReader.readFileStream(resourceToLoad, ifNoneMatch); + logService.debug(`loadLocalResource - Loaded using fileReader. requestUri=${requestUri}`); + + switch (contents.type) { + case WebviewFileReadResponse.Type.Success: + return new WebviewResourceResponse.StreamSuccess(contents.stream, contents.etag, mime); + + case WebviewFileReadResponse.Type.NotModified: + return new WebviewResourceResponse.NotModified(mime); + + default: + logService.error(`loadLocalResource - Unknown file read response`); + return WebviewResourceResponse.Failed; + } } catch (err) { + logService.debug(`loadLocalResource - Error using fileReader. requestUri=${requestUri}`); console.log(err); + return WebviewResourceResponse.Failed; } } diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts index 0fc108acf2..cc49032b7a 100644 --- a/src/vs/platform/webview/common/webviewManagerService.ts +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -19,6 +19,12 @@ export interface WebviewWindowId { readonly windowId: number; } +export type WebviewManagerDidLoadResourceResponse = + { buffer: VSBuffer, etag: string | undefined } + | 'not-modified' + | 'access-denied' + | 'not-found'; + export interface IWebviewManagerService { _serviceBrand: unknown; @@ -26,7 +32,7 @@ export interface IWebviewManagerService { unregisterWebview(id: string): Promise; updateWebviewMetadata(id: string, metadataDelta: Partial): Promise; - didLoadResource(requestId: number, content: VSBuffer | undefined): void; + didLoadResource(requestId: number, response: WebviewManagerDidLoadResourceResponse): void; setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise; } diff --git a/src/vs/platform/webview/common/webviewPortMapping.ts b/src/vs/platform/webview/common/webviewPortMapping.ts index 72512c66ef..35965e696a 100644 --- a/src/vs/platform/webview/common/webviewPortMapping.ts +++ b/src/vs/platform/webview/common/webviewPortMapping.ts @@ -19,7 +19,7 @@ export interface IWebviewPortMapping { */ export class WebviewPortMappingManager implements IDisposable { - private readonly _tunnels = new Map>(); + private readonly _tunnels = new Map(); constructor( private readonly _getExtensionLocation: () => URI | undefined, @@ -60,19 +60,19 @@ export class WebviewPortMappingManager implements IDisposable { return undefined; } - dispose() { + async dispose() { for (const tunnel of this._tunnels.values()) { - tunnel.then(tunnel => tunnel.dispose()); + await tunnel.dispose(); } this._tunnels.clear(); } - private getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise | undefined { + private async getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise { const existing = this._tunnels.get(remotePort); if (existing) { return existing; } - const tunnel = this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort); + const tunnel = await this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort); if (tunnel) { this._tunnels.set(remotePort, tunnel); } diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index 45f0605145..51ef50536a 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -3,14 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WebContents, webContents } from 'electron'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { session, WebContents, webContents } from 'electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; -import { IWebviewManagerService, RegisterWebviewMetadata, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; +import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; +import { IWebviewManagerService, RegisterWebviewMetadata, WebviewManagerDidLoadResourceResponse, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider'; import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; @@ -24,13 +25,23 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer constructor( @IFileService fileService: IFileService, + @ILogService logService: ILogService, @IRequestService requestService: IRequestService, @ITunnelService tunnelService: ITunnelService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); - this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService, windowsMainService)); + this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, logService, requestService, windowsMainService)); this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); + + const sess = session.fromPartition(webviewPartitionId); + sess.setPermissionRequestHandler((webContents, permission /* 'media' | 'geolocation' | 'notifications' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' */, callback) => { + return callback(false); + }); + + sess.setPermissionCheckHandler((webContents, permission /* 'media' */) => { + return false; + }); } public async registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise { @@ -93,7 +104,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer } } - public async didLoadResource(requestId: number, content: VSBuffer | undefined): Promise { - this.protocolProvider.didLoadResource(requestId, content); + public async didLoadResource(requestId: number, response: WebviewManagerDidLoadResourceResponse): Promise { + this.protocolProvider.didLoadResource(requestId, response); } } diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index e3b71d2f9f..5cd5e33958 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -5,14 +5,17 @@ import { protocol, session } from 'electron'; import { Readable } from 'stream'; -import { bufferToStream, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { bufferToStream, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; -import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; +import { loadLocalResource, readFileStream, WebviewFileReadResponse, webviewPartitionId, WebviewResourceFileReader, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; +import { WebviewManagerDidLoadResourceResponse } from 'vs/platform/webview/common/webviewManagerService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; interface WebviewMetadata { @@ -34,12 +37,13 @@ export class WebviewProtocolProvider extends Disposable { private readonly webviewMetadata = new Map(); private requestIdPool = 1; - private readonly pendingResourceReads = new Map void }>(); + private readonly pendingResourceReads = new Map void }>(); constructor( @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, @IRequestService private readonly requestService: IRequestService, - @IWindowsMainService readonly windowsMainService: IWindowsMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); @@ -125,7 +129,10 @@ export class WebviewProtocolProvider extends Disposable { } } - private async handleWebviewRequest(request: Electron.Request, callback: any) { + private async handleWebviewRequest( + request: Electron.ProtocolRequest, + callback: (response: string | Electron.ProtocolResponse) => void + ) { try { const uri = URI.parse(request.url); const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); @@ -144,11 +151,12 @@ export class WebviewProtocolProvider extends Disposable { } private async handleWebviewResourceRequest( - request: Electron.Request, - callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void + request: Electron.ProtocolRequest, + callback: (stream: NodeJS.ReadableStream | Electron.ProtocolResponse) => void ) { try { const uri = URI.parse(request.url); + const ifNoneMatch = request.headers['If-None-Match']; const id = uri.authority; const metadata = this.webviewMetadata.get(id); @@ -170,10 +178,10 @@ export class WebviewProtocolProvider extends Disposable { }; } - const fileService = { - readFileStream: async (resource: URI): Promise => { + const fileReader: WebviewResourceFileReader = { + readFileStream: async (resource: URI, etag: string | undefined): Promise => { if (resource.scheme === Schemas.file) { - return (await this.fileService.readFileStream(resource)).value; + return readFileStream(this.fileService, resource, etag); } // Unknown uri scheme. Try delegating the file read back to the renderer @@ -185,57 +193,112 @@ export class WebviewProtocolProvider extends Disposable { } const requestId = this.requestIdPool++; - const p = new Promise(resolve => { + const p = new Promise(resolve => { this.pendingResourceReads.set(requestId, { resolve }); }); window.send(`vscode:loadWebviewResource-${id}`, requestId, uri); const result = await p; - if (!result) { - throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND); - } + switch (result) { + case 'access-denied': + throw new FileOperationError('Could not read file', FileOperationResult.FILE_PERMISSION_DENIED); - return bufferToStream(result); + case 'not-found': + throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND); + + case 'not-modified': + return WebviewFileReadResponse.NotModified; + + default: + return new WebviewFileReadResponse.StreamSuccess(bufferToStream(result.buffer), result.etag); + } } }; - const result = await loadLocalResource(uri, { + const result = await loadLocalResource(uri, ifNoneMatch, { extensionLocation: metadata.extensionLocation, roots: metadata.localResourceRoots, remoteConnectionData: metadata.remoteConnectionData, rewriteUri, - }, fileService, this.requestService); + }, fileReader, this.requestService, this.logService, CancellationToken.None); - if (result.type === WebviewResourceResponse.Type.Success) { - return callback({ - statusCode: 200, - data: this.streamToNodeReadable(result.stream), - headers: { - 'Content-Type': result.mimeType, - 'Access-Control-Allow-Origin': '*', + switch (result.type) { + case WebviewFileReadResponse.Type.Success: + { + const cacheHeaders: Record = result.etag ? { + 'ETag': result.etag, + 'Cache-Control': 'no-cache' + } : {}; + + const ifNoneMatch = request.headers['If-None-Match']; + if (ifNoneMatch && result.etag === ifNoneMatch) { + /* + * Note that the server generating a 304 response MUST + * generate any of the following header fields that would + * have been sent in a 200 (OK) response to the same request: + * Cache-Control, Content-Location, Date, ETag, Expires, and Vary. + * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) + */ + return callback({ + statusCode: 304, // not modified + data: undefined, // The request fails if `data` is not set + headers: { + 'Content-Type': result.mimeType, + 'Access-Control-Allow-Origin': '*', + ...cacheHeaders + } + }); + } + + return callback({ + statusCode: 200, + data: this.streamToNodeReadable(result.stream), + headers: { + 'Content-Type': result.mimeType, + 'Access-Control-Allow-Origin': '*', + ...cacheHeaders + } + }); + } + case WebviewResourceResponse.Type.NotModified: + { + /* + * Note that the server generating a 304 response MUST + * generate any of the following header fields that would + * have been sent in a 200 (OK) response to the same request: + * Cache-Control, Content-Location, Date, ETag, Expires, and Vary. + * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) + */ + return callback({ + statusCode: 304, // not modified + data: undefined, // The request fails if `data` is not set + headers: { + 'Content-Type': result.mimeType, + 'Access-Control-Allow-Origin': '*', + } + }); + } + case WebviewResourceResponse.Type.AccessDenied: + { + console.error('Webview: Cannot load resource outside of protocol root'); + return callback({ data: undefined, statusCode: 401 }); } - }); - } - - if (result.type === WebviewResourceResponse.Type.AccessDenied) { - console.error('Webview: Cannot load resource outside of protocol root'); - return callback({ data: null, statusCode: 401 }); } } } catch { // noop } - return callback({ data: null, statusCode: 404 }); + return callback({ data: undefined, statusCode: 404 }); } - public didLoadResource(requestId: number, content: VSBuffer | undefined) { + public didLoadResource(requestId: number, response: WebviewManagerDidLoadResourceResponse) { const pendingRead = this.pendingResourceReads.get(requestId); if (!pendingRead) { throw new Error('Unknown request'); } this.pendingResourceReads.delete(requestId); - pendingRead.resolve(content); + pendingRead.resolve(response); } } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 52b4ca4407..62571e47d3 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -9,7 +9,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { LogLevel } from 'vs/platform/log/common/log'; -import { ExportData } from 'vs/base/common/performance'; +import { PerformanceMark } from 'vs/base/common/performance'; export const WindowMinimumSize = { WIDTH: 400, @@ -18,38 +18,37 @@ export const WindowMinimumSize = { }; export interface IBaseOpenWindowsOptions { - forceReuseWindow?: boolean; + readonly forceReuseWindow?: boolean; } export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { - forceNewWindow?: boolean; - preferNewWindow?: boolean; + readonly forceNewWindow?: boolean; + readonly preferNewWindow?: boolean; - noRecentEntry?: boolean; + readonly noRecentEntry?: boolean; - addMode?: boolean; + readonly addMode?: boolean; - diffMode?: boolean; - gotoLineMode?: boolean; + readonly diffMode?: boolean; + readonly gotoLineMode?: boolean; - waitMarkerFileURI?: URI; + readonly waitMarkerFileURI?: URI; } export interface IAddFoldersRequest { - foldersToAdd: UriComponents[]; + readonly foldersToAdd: UriComponents[]; } export interface IOpenedWindow { - id: number; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - title: string; - filename?: string; - dirty: boolean; + readonly id: number; + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; + readonly title: string; + readonly filename?: string; + readonly dirty: boolean; } export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { - remoteAuthority?: string; + readonly remoteAuthority?: string; } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; @@ -59,15 +58,15 @@ export interface IBaseWindowOpenable { } export interface IWorkspaceToOpen extends IBaseWindowOpenable { - workspaceUri: URI; + readonly workspaceUri: URI; } export interface IFolderToOpen extends IBaseWindowOpenable { - folderUri: URI; + readonly folderUri: URI; } export interface IFileToOpen extends IBaseWindowOpenable { - fileUri: URI; + readonly fileUri: URI; } export function isWorkspaceToOpen(uriToOpen: IWindowOpenable): uriToOpen is IWorkspaceToOpen { @@ -82,40 +81,39 @@ export function isFileToOpen(uriToOpen: IWindowOpenable): uriToOpen is IFileToOp return !!(uriToOpen as IFileToOpen).fileUri; } -export type MenuBarVisibility = 'default' | 'visible' | 'toggle' | 'hidden' | 'compact'; +export type MenuBarVisibility = 'classic' | 'visible' | 'toggle' | 'hidden' | 'compact'; export function getMenuBarVisibility(configurationService: IConfigurationService): MenuBarVisibility { const titleBarStyle = getTitleBarStyle(configurationService); - const menuBarVisibility = configurationService.getValue('window.menuBarVisibility'); + const menuBarVisibility = configurationService.getValue('window.menuBarVisibility'); - if (titleBarStyle === 'native' && menuBarVisibility === 'compact') { - return 'default'; + if (menuBarVisibility === 'default' || (titleBarStyle === 'native' && menuBarVisibility === 'compact')) { + return 'classic'; } else { return menuBarVisibility; } } export interface IWindowsConfiguration { - window: IWindowSettings; + readonly window: IWindowSettings; } export interface IWindowSettings { - openFilesInNewWindow: 'on' | 'off' | 'default'; - openFoldersInNewWindow: 'on' | 'off' | 'default'; - openWithoutArgumentsInNewWindow: 'on' | 'off'; - restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; - restoreFullscreen: boolean; - zoomLevel: number; - titleBarStyle: 'native' | 'custom'; - autoDetectHighContrast: boolean; - menuBarVisibility: MenuBarVisibility; - newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; - nativeTabs: boolean; - nativeFullScreen: boolean; - enableMenuBarMnemonics: boolean; - closeWhenEmpty: boolean; - clickThroughInactive: boolean; - enableExperimentalProxyLoginDialog: boolean; + readonly openFilesInNewWindow: 'on' | 'off' | 'default'; + readonly openFoldersInNewWindow: 'on' | 'off' | 'default'; + readonly openWithoutArgumentsInNewWindow: 'on' | 'off'; + readonly restoreWindows: 'preserve' | 'all' | 'folders' | 'one' | 'none'; + readonly restoreFullscreen: boolean; + readonly zoomLevel: number; + readonly titleBarStyle: 'native' | 'custom'; + readonly autoDetectHighContrast: boolean; + readonly menuBarVisibility: MenuBarVisibility; + readonly newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen'; + readonly nativeTabs: boolean; + readonly nativeFullScreen: boolean; + readonly enableMenuBarMnemonics: boolean; + readonly closeWhenEmpty: boolean; + readonly clickThroughInactive: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -154,24 +152,24 @@ export interface IPath extends IPathData { export interface IPathData { // the file path to open within the instance - fileUri?: UriComponents; + readonly fileUri?: UriComponents; // the line number in the file path to open - lineNumber?: number; + readonly lineNumber?: number; // the column number in the file path to open - columnNumber?: number; + readonly columnNumber?: number; // a hint that the file exists. if true, the // file exists, if false it does not. with // undefined the state is unknown. - exists?: boolean; + readonly exists?: boolean; // Specifies if the file should be only be opened if it exists - openOnlyIfExists?: boolean; + readonly openOnlyIfExists?: boolean; // Specifies an optional id to override the editor used to edit the resource, e.g. custom editor. - overrideId?: string; + readonly overrideId?: string; } export interface IPathsToWaitFor extends IPathsToWaitForData { @@ -180,36 +178,36 @@ export interface IPathsToWaitFor extends IPathsToWaitForData { } interface IPathsToWaitForData { - paths: IPathData[]; - waitMarkerFileUri: UriComponents; + readonly paths: IPathData[]; + readonly waitMarkerFileUri: UriComponents; } export interface IOpenFileRequest { - filesToOpenOrCreate?: IPathData[]; - filesToDiff?: IPathData[]; + readonly filesToOpenOrCreate?: IPathData[]; + readonly filesToDiff?: IPathData[]; } /** * Additional context for the request on native only. */ export interface INativeOpenFileRequest extends IOpenFileRequest { - termProgram?: string; - filesToWait?: IPathsToWaitForData; + readonly termProgram?: string; + readonly filesToWait?: IPathsToWaitForData; } export interface INativeRunActionInWindowRequest { - id: string; - from: 'menu' | 'touchbar' | 'mouse'; - args?: any[]; + readonly id: string; + readonly from: 'menu' | 'touchbar' | 'mouse'; + readonly args?: any[]; } export interface INativeRunKeybindingInWindowRequest { - userSettingsLabel: string; + readonly userSettingsLabel: string; } export interface IColorScheme { - dark: boolean; - highContrast: boolean; + readonly dark: boolean; + readonly highContrast: boolean; } export interface IWindowConfiguration { @@ -225,7 +223,7 @@ export interface IWindowConfiguration { } export interface IOSConfiguration { - release: string; + readonly release: string; } export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs { @@ -241,8 +239,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native nodeCachedDataDir?: string; partsSplashPath: string; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; + workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; isInitialStartup?: boolean; logLevel: LogLevel; @@ -250,7 +247,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native fullscreen?: boolean; maximized?: boolean; accessibilitySupport?: boolean; - perfEntries: ExportData; + perfMarks: PerformanceMark[]; userEnv: IProcessEnvironment; filesToWait?: IPathsToWaitFor; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index b883bb3bff..1fb1cf7a4a 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,18 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowOpenable, IOpenEmptyWindowOptions, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { URI } from 'vs/base/common/uri'; import { Rectangle, BrowserWindow, WebContents } from 'electron'; import { IDisposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; +export const enum OpenContext { + + // opening when running from the command line + CLI, + + // macOS only: opening from the dock (also when opening files to a running instance from desktop) + DOCK, + + // opening from the main application window + MENU, + + // opening from a file or folder dialog + DIALOG, + + // opening from the OS's UI + DESKTOP, + + // opening through the API + API +} + export interface IWindowState { width?: number; height?: number; @@ -45,8 +65,8 @@ export interface ICodeWindow extends IDisposable { readonly win: BrowserWindow; readonly config: INativeWindowConfiguration | undefined; - readonly openedFolderUri?: URI; - readonly openedWorkspace?: IWorkspaceIdentifier; + readonly openedWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; + readonly backupPath?: string; readonly remoteAuthority?: string; @@ -64,7 +84,7 @@ export interface ICodeWindow extends IDisposable { addTabbedWindow(window: ICodeWindow): void; - load(config: INativeWindowConfiguration, isReload?: boolean): void; + load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void; reload(cli?: NativeParsedArgs): void; focus(options?: { force: boolean }): void; @@ -104,9 +124,11 @@ export interface IWindowsMainService { readonly _serviceBrand: undefined; + readonly onWindowsCountChanged: Event; + readonly onWindowOpened: Event; readonly onWindowReady: Event; - readonly onWindowsCountChanged: Event; + readonly onWindowDestroyed: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; @@ -115,13 +137,14 @@ export interface IWindowsMainService { sendToFocused(channel: string, ...args: any[]): void; sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; + getWindows(): ICodeWindow[]; + getWindowCount(): number; + getFocusedWindow(): ICodeWindow | undefined; getLastActiveWindow(): ICodeWindow | undefined; getWindowById(windowId: number): ICodeWindow | undefined; getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined; - getWindows(): ICodeWindow[]; - getWindowCount(): number; } export interface IBaseOpenConfiguration { diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts new file mode 100644 index 0000000000..c2b4ad1c9b --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceIdentifier, IResolvedWorkspace, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; + +export function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): ICodeWindow | undefined { + + // First check for windows with workspaces that have a parent folder of the provided path opened + for (const window of windows) { + const workspace = window.openedWorkspace; + if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = localWorkspaceResolver(workspace); + + // resolved workspace: folders are known and can be compared with + if (resolvedWorkspace) { + if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { + return window; + } + } + + // unresolved: can only compare with workspace location + else { + if (extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, workspace.configPath)) { + return window; + } + } + } + } + + // Then go with single folder windows that are parent of the provided file path + const singleFolderWindowsOnFilePath = windows.filter(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedWorkspace.uri)); + if (singleFolderWindowsOnFilePath.length) { + return singleFolderWindowsOnFilePath.sort((windowA, windowB) => -((windowA.openedWorkspace as ISingleFolderWorkspaceIdentifier).uri.path.length - (windowB.openedWorkspace as ISingleFolderWorkspaceIdentifier).uri.path.length))[0]; + } + + return undefined; +} + +export function findWindowOnWorkspaceOrFolder(windows: ICodeWindow[], folderOrWorkspaceConfigUri: URI): ICodeWindow | undefined { + + for (const window of windows) { + + // check for workspace config path + if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, folderOrWorkspaceConfigUri)) { + return window; + } + + // check for folder path + if (isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderOrWorkspaceConfigUri)) { + return window; + } + } + + return undefined; +} + + +export function findWindowOnExtensionDevelopmentPath(windows: ICodeWindow[], extensionDevelopmentPaths: string[]): ICodeWindow | undefined { + + const matches = (uriString: string): boolean => { + return extensionDevelopmentPaths.some(path => extUriBiasedIgnorePathCase.isEqual(URI.file(path), URI.file(uriString))); + }; + + for (const window of windows) { + + // match on extension development path. the path can be one or more paths + // so we check if any of the paths match on any of the provided ones + if (window.config?.extensionDevelopmentPath?.some(path => matches(path))) { + return window; + } + } + + return undefined; +} diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index afe4fcd3dd..b3d189d4d9 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,175 +3,135 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import { statSync } from 'fs'; import { basename, normalize, join, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; +import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IStateService } from 'vs/platform/state/node/state'; -import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; -import { screen, BrowserWindow, MessageBoxOptions, Display, app, WebContents } from 'electron'; +import { CodeWindow } from 'vs/code/electron-main/window'; +import { BrowserWindow, MessageBoxOptions, WebContents } from 'electron'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, OpenContext } from 'vs/platform/windows/node/window'; +import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { findWindowOnFile, findWindowOnWorkspaceOrFolder, findWindowOnExtensionDevelopmentPath } from 'vs/platform/windows/electron-main/windowsFinder'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; -import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IOpenEmptyConfiguration, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; +import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { IWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { normalizePath, originalFSPath, removeTrailingPathSeparator, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; -import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage'; -import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWindowState, WindowsStateHandler } from 'vs/platform/windows/electron-main/windowsStateHandler'; +import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier, IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; +import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware, sanitizeFilePath } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; import { getPathLabel } from 'vs/base/common/labels'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IFileService } from 'vs/platform/files/common/files'; -export interface IWindowState { - workspace?: IWorkspaceIdentifier; - folderUri?: URI; - backupPath?: string; - remoteAuthority?: string; - uiState: ISingleWindowState; -} - -export interface IWindowsState { - lastActiveWindow?: IWindowState; - lastPluginDevelopmentHostWindow?: IWindowState; - openedWindows: IWindowState[]; -} - -interface INewWindowState extends ISingleWindowState { - hasDefaultState?: boolean; -} +//#region Helper Interfaces type RestoreWindowsSetting = 'preserve' | 'all' | 'folders' | 'one' | 'none'; interface IOpenBrowserWindowOptions { - userEnv?: IProcessEnvironment; - cli?: NativeParsedArgs; + readonly userEnv?: IProcessEnvironment; + readonly cli?: NativeParsedArgs; - workspace?: IWorkspaceIdentifier; - folderUri?: URI; + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - remoteAuthority?: string; + readonly remoteAuthority?: string; - initialStartup?: boolean; + readonly initialStartup?: boolean; - filesToOpen?: IFilesToOpen; + readonly filesToOpen?: IFilesToOpen; - forceNewWindow?: boolean; - forceNewTabbedWindow?: boolean; - windowToUse?: ICodeWindow; + readonly forceNewWindow?: boolean; + readonly forceNewTabbedWindow?: boolean; + readonly windowToUse?: ICodeWindow; - emptyWindowBackupInfo?: IEmptyWindowBackupInfo; + readonly emptyWindowBackupInfo?: IEmptyWindowBackupInfo; } -interface IPathParseOptions { - ignoreFileNotFound?: boolean; - gotoLineMode?: boolean; - remoteAuthority?: string; +interface IPathResolveOptions { + readonly ignoreFileNotFound?: boolean; + readonly gotoLineMode?: boolean; + readonly remoteAuthority?: string; } interface IFilesToOpen { + readonly remoteAuthority?: string; + filesToOpenOrCreate: IPath[]; filesToDiff: IPath[]; filesToWait?: IPathsToWaitFor; - remoteAuthority?: string; } interface IPathToOpen extends IPath { - // the workspace for a Code instance to open - workspace?: IWorkspaceIdentifier; + // the workspace to open + readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - // the folder path for a Code instance to open - folderUri?: URI; - - // the backup path for a Code instance to use - backupPath?: string; + // the backup path to use + readonly backupPath?: string; // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; + readonly remoteAuthority?: string; // optional label for the recent history label?: string; } -function isFolderPathToOpen(path: IPathToOpen): path is IFolderPathToOpen { - return !!path.folderUri; +interface IWorkspacePathToOpen extends IPathToOpen { + readonly workspace: IWorkspaceIdentifier; } -interface IFolderPathToOpen { - - // the folder path for a Code instance to open - folderUri: URI; - - // the backup path for a Code instance to use - backupPath?: string; - - // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; - - // optional label for the recent history - label?: string; +interface ISingleFolderWorkspacePathToOpen extends IPathToOpen { + readonly workspace: ISingleFolderWorkspaceIdentifier; } -function isWorkspacePathToOpen(path: IPathToOpen): path is IWorkspacePathToOpen { - return !!path.workspace; +function isWorkspacePathToOpen(path: IPathToOpen | undefined): path is IWorkspacePathToOpen { + return isWorkspaceIdentifier(path?.workspace); } -interface IWorkspacePathToOpen { - - // the workspace for a Code instance to open - workspace: IWorkspaceIdentifier; - - // the backup path for a Code instance to use - backupPath?: string; - - // the remote authority for the Code instance to open. Undefined if not remote. - remoteAuthority?: string; - - // optional label for the recent history - label?: string; +function isSingleFolderWorkspacePathToOpen(path: IPathToOpen | undefined): path is ISingleFolderWorkspacePathToOpen { + return isSingleFolderWorkspaceIdentifier(path?.workspace); } +//#endregion + export class WindowsMainService extends Disposable implements IWindowsMainService { declare readonly _serviceBrand: undefined; - private static readonly windowsStateStorageKey = 'windowsState'; - private static readonly WINDOWS: ICodeWindow[] = []; - private readonly windowsState: IWindowsState; - private lastClosedWindowState?: IWindowState; - - private shuttingDown = false; - private readonly _onWindowOpened = this._register(new Emitter()); readonly onWindowOpened = this._onWindowOpened.event; private readonly _onWindowReady = this._register(new Emitter()); readonly onWindowReady = this._onWindowReady.event; + private readonly _onWindowDestroyed = this._register(new Emitter()); + readonly onWindowDestroyed = this._onWindowDestroyed.event; + private readonly _onWindowsCountChanged = this._register(new Emitter()); readonly onWindowsCountChanged = this._onWindowsCountChanged.event; + private readonly windowsStateHandler = this._register(new WindowsStateHandler(this, this.stateService, this.lifecycleMainService, this.logService, this.configurationService)); + constructor( private readonly machineId: string, private readonly initialUserEnv: IProcessEnvironment, @@ -182,190 +142,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic @IBackupMainService private readonly backupMainService: IBackupMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @IFileService private readonly fileService: IFileService ) { super(); - this.windowsState = restoreWindowsState(this.stateService.getItem(WindowsMainService.windowsStateStorageKey)); - if (!Array.isArray(this.windowsState.openedWindows)) { - this.windowsState.openedWindows = []; - } - this.lifecycleMainService.when(LifecycleMainPhase.Ready).then(() => this.registerListeners()); - this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.installWindowsMutex()); - } - - private installWindowsMutex(): void { - const win32MutexName = product.win32MutexName; - if (isWindows && win32MutexName) { - try { - const WindowsMutex = (require.__$__nodeRequire('windows-mutex') as typeof import('windows-mutex')).Mutex; - const mutex = new WindowsMutex(win32MutexName); - once(this.lifecycleMainService.onWillShutdown)(() => mutex.release()); - } catch (e) { - this.logService.error(e); - } - } } private registerListeners(): void { - // When a window looses focus, save all windows state. This allows to - // prevent loss of window-state data when OS is restarted without properly - // shutting down the application (https://github.com/microsoft/vscode/issues/87171) - app.on('browser-window-blur', () => { - if (!this.shuttingDown) { - this.saveWindowsState(); - } - }); - - // Handle various lifecycle events around windows - this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); - this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); - this.onWindowsCountChanged(e => { - if (e.newCount - e.oldCount > 0) { - // clear last closed window state when a new window opens. this helps on macOS where - // otherwise closing the last window, opening a new window and then quitting would - // use the state of the previously closed window when restarting. - this.lastClosedWindowState = undefined; - } - }); - // Signal a window is ready after having entered a workspace - this._register(this.workspacesMainService.onWorkspaceEntered(event => { - this._onWindowReady.fire(event.window); - })); - } - - // Note that onBeforeShutdown() and onBeforeWindowClose() 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 - // - 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() - // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() - // and then onBeforeWindowClose(). - // - // Here is the behavior on different OS depending on action taken (Electron 1.7.x): - // - // Legend - // - quit(N): quit application with N windows opened - // - 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 - // - // macOS - // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) - // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) - // - quit(0): onBeforeShutdown(0) - // - close(1): onBeforeWindowClose(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) - // - // 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) - // - private onBeforeShutdown(): void { - this.shuttingDown = true; - - this.saveWindowsState(); - } - - private saveWindowsState(): void { - const currentWindowsState: IWindowsState = { - openedWindows: [], - lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow, - lastActiveWindow: this.lastClosedWindowState - }; - - // 1.) Find a last active window (pick any other first window otherwise) - if (!currentWindowsState.lastActiveWindow) { - let activeWindow = this.getLastActiveWindow(); - if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { - activeWindow = WindowsMainService.WINDOWS.find(window => !window.isExtensionDevelopmentHost); - } - - if (activeWindow) { - currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); - } - } - - // 2.) Find extension host window - const extensionHostWindow = WindowsMainService.WINDOWS.find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); - if (extensionHostWindow) { - currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); - } - - // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update - // - // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) - // so if we ever want to persist the UI state of the last closed window (window count === 1), it has - // to come from the stored lastClosedWindowState on Win/Linux at least - if (this.getWindowCount() > 1) { - currentWindowsState.openedWindows = WindowsMainService.WINDOWS.filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); - } - - // Persist - const state = getWindowsStateStoreData(currentWindowsState); - this.stateService.setItem(WindowsMainService.windowsStateStorageKey, state); - - if (this.shuttingDown) { - this.logService.trace('onBeforeShutdown', state); - } - } - - // See note on #onBeforeShutdown() for details how these events are flowing - private onBeforeWindowClose(win: ICodeWindow): void { - if (this.lifecycleMainService.quitRequested) { - return; // during quit, many windows close in parallel so let it be handled in the before-quit handler - } - - // On Window close, update our stored UI state of this window - const state: IWindowState = this.toWindowState(win); - if (win.isExtensionDevelopmentHost && !win.isExtensionTestHost) { - this.windowsState.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state - } - - // Any non extension host window with same workspace or folder - else if (!win.isExtensionDevelopmentHost && (!!win.openedWorkspace || !!win.openedFolderUri)) { - this.windowsState.openedWindows.forEach(o => { - const sameWorkspace = win.openedWorkspace && o.workspace && o.workspace.id === win.openedWorkspace.id; - const sameFolder = win.openedFolderUri && o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, win.openedFolderUri); - - if (sameWorkspace || sameFolder) { - o.uiState = state.uiState; - } - }); - } - - // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state - // before quitting, we need to remember the UI state of this window to be able to persist it. - // On macOS we keep the last closed window state ready in case the user wants to quit right after or - // wants to open another window, in which case we use this state over the persisted one. - if (this.getWindowCount() === 1) { - this.lastClosedWindowState = state; - } - } - - private toWindowState(win: ICodeWindow): IWindowState { - return { - workspace: win.openedWorkspace, - folderUri: win.openedFolderUri, - backupPath: win.backupPath, - remoteAuthority: win.remoteAuthority, - uiState: win.serializeWindowState() - }; + this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this._onWindowReady.fire(event.window))); } openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { @@ -375,18 +165,22 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cli = { ...cli, remote }; } + const forceEmpty = true; const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ ...openConfig, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); + return this.open({ ...openConfig, cli, forceEmpty, forceNewWindow, forceReuseWindow }); } open(openConfig: IOpenConfiguration): ICodeWindow[] { this.logService.trace('windowsManager#open'); - openConfig = this.validateOpenConfig(openConfig); - const foldersToAdd: IFolderPathToOpen[] = []; - const foldersToOpen: IFolderPathToOpen[] = []; + if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { + openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window + } + + const foldersToAdd: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToOpen: ISingleFolderWorkspacePathToOpen[] = []; const workspacesToOpen: IWorkspacePathToOpen[] = []; const workspacesToRestore: IWorkspacePathToOpen[] = []; const emptyToRestore: IEmptyWindowBackupInfo[] = []; @@ -397,7 +191,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const pathsToOpen = this.getPathsToOpen(openConfig); this.logService.trace('windowsManager#open pathsToOpen', pathsToOpen); for (const path of pathsToOpen) { - if (isFolderPathToOpen(path)) { + if (isSingleFolderWorkspacePathToOpen(path)) { if (openConfig.addMode) { // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. @@ -431,13 +225,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; } - // // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) - // if (openConfig.initialStartup) { // Untitled workspaces are always restored - workspacesToRestore.push(...this.workspacesMainService.getUntitledWorkspacesSync()); + workspacesToRestore.push(...this.workspacesManagementMainService.getUntitledWorkspacesSync()); workspacesToOpen.push(...workspacesToRestore); // Empty windows with backups are always restored @@ -447,46 +239,55 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, filesToOpen, foldersToAdd); + const { windows: usedWindows, filesOpenedInWindow } = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, filesToOpen, foldersToAdd); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyToRestore.length}, emptyToOpen: ${emptyToOpen})`); // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); - let focusLastOpened = true; - let focusLastWindow = true; - // 1.) focus last active window if we are not instructed to open any paths - if (focusLastActive) { - const lastActiveWindow = usedWindows.filter(window => this.windowsState.lastActiveWindow && window.backupPath === this.windowsState.lastActiveWindow.backupPath); - if (lastActiveWindow.length) { - lastActiveWindow[0].focus(); - focusLastOpened = false; - focusLastWindow = false; - } + // 1.) focus window we opened files in always with highest priority + if (filesOpenedInWindow) { + filesOpenedInWindow.focus(); } - // 2.) if instructed to open paths, focus last window which is not restored - if (focusLastOpened) { - for (let i = usedWindows.length - 1; i >= 0; i--) { - const usedWindow = usedWindows[i]; - if ( - (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace - (usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window - ) { - continue; + // Otherwise, find a good window based on open params + else { + const focusLastActive = this.windowsStateHandler.state.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); + let focusLastOpened = true; + let focusLastWindow = true; + + // 2.) focus last active window if we are not instructed to open any paths + if (focusLastActive) { + const lastActiveWindow = usedWindows.filter(window => this.windowsStateHandler.state.lastActiveWindow && window.backupPath === this.windowsStateHandler.state.lastActiveWindow.backupPath); + if (lastActiveWindow.length) { + lastActiveWindow[0].focus(); + focusLastOpened = false; + focusLastWindow = false; } - - usedWindow.focus(); - focusLastWindow = false; - break; } - } - // 3.) finally, always ensure to have at least last used window focused - if (focusLastWindow) { - usedWindows[usedWindows.length - 1].focus(); + // 3.) if instructed to open paths, focus last window which is not restored + if (focusLastOpened) { + for (let i = usedWindows.length - 1; i >= 0; i--) { + const usedWindow = usedWindows[i]; + if ( + (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace + (usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window + ) { + continue; + } + + usedWindow.focus(); + focusLastWindow = false; + break; + } + } + + // 4.) finally, always ensure to have at least last used window focused + if (focusLastWindow) { + usedWindows[usedWindows.length - 1].focus(); + } } } @@ -495,11 +296,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0; if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !openConfig.noRecentEntry) { const recents: IRecent[] = []; - for (let pathToOpen of pathsToOpen) { - if (pathToOpen.workspace) { + for (const pathToOpen of pathsToOpen) { + if (isWorkspacePathToOpen(pathToOpen)) { recents.push({ label: pathToOpen.label, workspace: pathToOpen.workspace }); - } else if (pathToOpen.folderUri) { - recents.push({ label: pathToOpen.label, folderUri: pathToOpen.folderUri }); + } else if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { + recents.push({ label: pathToOpen.label, folderUri: pathToOpen.workspace.uri }); } else if (pathToOpen.fileUri) { recents.push({ label: pathToOpen.label, fileUri: pathToOpen.fileUri }); } @@ -513,32 +314,34 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { - usedWindows[0].whenClosedOrLoaded.then(() => fs.unlink(waitMarkerFileURI.fsPath, _error => undefined)); + usedWindows[0].whenClosedOrLoaded.then(() => this.fileService.del(waitMarkerFileURI), () => undefined); } return usedWindows; } - private validateOpenConfig(config: IOpenConfiguration): IOpenConfiguration { - - // Make sure addMode is only enabled if we have an active window - if (config.addMode && (config.initialStartup || !this.getLastActiveWindow())) { - config.addMode = false; - } - - return config; - } - private doOpen( openConfig: IOpenConfiguration, workspacesToOpen: IWorkspacePathToOpen[], - foldersToOpen: IFolderPathToOpen[], + foldersToOpen: ISingleFolderWorkspacePathToOpen[], emptyToRestore: IEmptyWindowBackupInfo[], emptyToOpen: number, filesToOpen: IFilesToOpen | undefined, - foldersToAdd: IFolderPathToOpen[] - ) { + foldersToAdd: ISingleFolderWorkspacePathToOpen[] + ): { windows: ICodeWindow[], filesOpenedInWindow: ICodeWindow | undefined } { + + // Keep track of used windows and remember + // if files have been opened in one of them const usedWindows: ICodeWindow[] = []; + let filesOpenedInWindow: ICodeWindow | undefined = undefined; + function addUsedWindow(window: ICodeWindow, openedFiles?: boolean): void { + usedWindows.push(window); + + if (openedFiles) { + filesOpenedInWindow = window; + filesToOpen = undefined; // reset `filesToOpen` since files have been opened + } + } // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); @@ -548,55 +351,59 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const authority = foldersToAdd[0].remoteAuthority; const lastActiveWindow = this.getLastActiveWindowForAuthority(authority); if (lastActiveWindow) { - usedWindows.push(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(f => f.folderUri))); + addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri))); } } - // Handle files to open/diff or to create when we dont open a folder and we do not restore any folder/untitled from hot-exit - const potentialWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; - if (potentialWindowsCount === 0 && filesToOpen) { + // Handle files to open/diff or to create when we dont open a folder and we do not restore any + // folder/untitled from hot-exit by trying to open them in the window that fits best + const potentialNewWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length; + if (filesToOpen && potentialNewWindowsCount === 0) { // Find suitable window or folder path to open files in const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0]; // only look at the windows with correct authority - const windows = WindowsMainService.WINDOWS.filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); + const windows = this.getWindows().filter(window => filesToOpen && window.remoteAuthority === filesToOpen.remoteAuthority); - const bestWindowOrFolder = findBestWindowOrFolderForFile({ - windows, - newWindow: openFilesInNewWindow, - context: openConfig.context, - fileUri: fileToCheck?.fileUri, - localWorkspaceResolver: workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath) : null - }); + // figure out a good window to open the files in if any + // with a fallback to the last active window. + // + // in case `openFilesInNewWindow` is enforced, we skip + // this step. + let windowToUseForFiles: ICodeWindow | undefined = undefined; + if (fileToCheck?.fileUri && !openFilesInNewWindow) { + if (openConfig.context === OpenContext.DESKTOP || openConfig.context === OpenContext.CLI || openConfig.context === OpenContext.DOCK) { + windowToUseForFiles = findWindowOnFile(windows, fileToCheck.fileUri, workspace => workspace.configPath.scheme === Schemas.file ? this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath) : null); + } + + if (!windowToUseForFiles) { + windowToUseForFiles = this.doGetLastActiveWindow(windows); + } + } // We found a window to open the files in - if (bestWindowOrFolder instanceof CodeWindow) { + if (windowToUseForFiles) { // Window is workspace - if (bestWindowOrFolder.openedWorkspace) { - workspacesToOpen.push({ workspace: bestWindowOrFolder.openedWorkspace, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + if (isWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { + workspacesToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is single folder - else if (bestWindowOrFolder.openedFolderUri) { - foldersToOpen.push({ folderUri: bestWindowOrFolder.openedFolderUri, remoteAuthority: bestWindowOrFolder.remoteAuthority }); + else if (isSingleFolderWorkspaceIdentifier(windowToUseForFiles.openedWorkspace)) { + foldersToOpen.push({ workspace: windowToUseForFiles.openedWorkspace, remoteAuthority: windowToUseForFiles.remoteAuthority }); } // Window is empty else { - - // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen)); - - // Reset these because we handled them - filesToOpen = undefined; + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowToUseForFiles, filesToOpen), true); } } // Finally, if no window or folder is found, just open the files in an empty window else { - usedWindows.push(this.openInBrowserWindow({ + addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -604,37 +411,29 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, remoteAuthority: filesToOpen.remoteAuthority, forceNewTabbedWindow: openConfig.forceNewTabbedWindow - })); - - // Reset these because we handled them - filesToOpen = undefined; + }), true); } } // Handle workspaces to open (instructed and to restore) - const allWorkspacesToOpen = arrays.distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates + const allWorkspacesToOpen = distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates if (allWorkspacesToOpen.length > 0) { // Check for existing instances - const windowsOnWorkspace = arrays.coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); + const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), workspaceToOpen.workspace.configPath))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow)); - - // Reset these because we handled them - if (filesToOpenInWindow) { - filesToOpen = undefined; - } + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones allWorkspacesToOpen.forEach(workspaceToOpen => { - if (windowsOnWorkspace.some(win => win.openedWorkspace && win.openedWorkspace.id === workspaceToOpen.workspace.id)) { + if (windowsOnWorkspace.some(window => window.openedWorkspace && window.openedWorkspace.id === workspaceToOpen.workspace.id)) { return; // ignore folders that are already open } @@ -642,42 +441,31 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow)); - - // Reset these because we handled them - if (filesToOpenInWindow) { - filesToOpen = undefined; - } + addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); } // Handle folders to open (instructed and to restore) - const allFoldersToOpen = arrays.distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.folderUri)); // prevent duplicates + const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.workspace.uri)); // prevent duplicates if (allFoldersToOpen.length > 0) { // Check for existing instances - const windowsOnFolderPath = arrays.coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); + const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspaceOrFolder(this.getWindows(), folderToOpen.workspace.uri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow)); - - // Reset these because we handled them - if (filesToOpenInWindow) { - filesToOpen = undefined; - } + addUsedWindow(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then } // Open remaining ones allFoldersToOpen.forEach(folderToOpen => { - - if (windowsOnFolderPath.some(win => extUriBiasedIgnorePathCase.isEqual(win.openedFolderUri, folderToOpen.folderUri))) { + if (windowsOnFolderPath.some(window => isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.uri, folderToOpen.workspace.uri))) { return; // ignore folders that are already open } @@ -685,25 +473,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow)); - - // Reset these because we handled them - if (filesToOpenInWindow) { - filesToOpen = undefined; - } + addUsedWindow(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); } // Handle empty to restore - const allEmptyToRestore = arrays.distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates + const allEmptyToRestore = distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates if (allEmptyToRestore.length > 0) { allEmptyToRestore.forEach(emptyWindowBackupInfo => { const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; - usedWindows.push(this.openInBrowserWindow({ + addUsedWindow(this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -712,12 +495,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, emptyWindowBackupInfo - })); - - // Reset these because we handled them - if (filesToOpenInWindow) { - filesToOpen = undefined; - } + }), !!filesToOpenInWindow); openFolderInNewWindow = true; // any other folders to open must open in new window then }); @@ -732,15 +510,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen)); + addUsedWindow(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen), !!filesToOpen); - // Reset these because we handled them - filesToOpen = undefined; - openFolderInNewWindow = true; // any other window to open must open in new window then + // any other window to open must open in new window then + openFolderInNewWindow = true; } } - return arrays.distinct(usedWindows); + return { windows: distinct(usedWindows), filesOpenedInWindow }; } private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, filesToOpen?: IFilesToOpen): ICodeWindow { @@ -748,23 +525,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.focus(); // make sure window has focus - const params: { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {}; - if (filesToOpen) { - params.filesToOpenOrCreate = filesToOpen.filesToOpenOrCreate; - params.filesToDiff = filesToOpen.filesToDiff; - params.filesToWait = filesToOpen.filesToWait; - } - - if (configuration.userEnv) { - params.termProgram = configuration.userEnv['TERM_PROGRAM']; - } - + const params: INativeOpenFileRequest = { + filesToOpenOrCreate: filesToOpen?.filesToOpenOrCreate, + filesToDiff: filesToOpen?.filesToDiff, + filesToWait: filesToOpen?.filesToWait, + termProgram: configuration?.userEnv?.['TERM_PROGRAM'] + }; window.sendWhenReady('vscode:openFiles', CancellationToken.None, params); return window; } private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { + this.logService.trace('windowsManager#doAddFoldersToExistingWindow'); + window.focus(); // make sure window has focus const request: IAddFoldersRequest = { foldersToAdd }; @@ -790,50 +564,57 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); } - private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IWorkspacePathToOpen | ISingleFolderWorkspacePathToOpen, forceNewWindow: boolean, filesToOpen: IFilesToOpen | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/49587 } return this.openInBrowserWindow({ + workspace: folderOrWorkspace.workspace, userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, - workspace: folderOrWorkspace.workspace, - folderUri: folderOrWorkspace.folderUri, - filesToOpen, remoteAuthority: folderOrWorkspace.remoteAuthority, forceNewWindow, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, + filesToOpen, windowToUse }); } private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] { - let windowsToOpen: IPathToOpen[]; + let pathsToOpen: IPathToOpen[]; let isCommandLineOrAPICall = false; let restoredWindows = false; // Extract paths: from API if (openConfig.urisToOpen && openConfig.urisToOpen.length > 0) { - windowsToOpen = this.doExtractPathsFromAPI(openConfig); + pathsToOpen = this.doExtractPathsFromAPI(openConfig); isCommandLineOrAPICall = true; } // Check for force empty else if (openConfig.forceEmpty) { - windowsToOpen = [Object.create(null)]; + pathsToOpen = [Object.create(null)]; } // Extract paths: from CLI else if (openConfig.cli._.length || openConfig.cli['folder-uri'] || openConfig.cli['file-uri']) { - windowsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + pathsToOpen = this.doExtractPathsFromCLI(openConfig.cli); + if (pathsToOpen.length === 0) { + pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to open from command line + } + isCommandLineOrAPICall = true; } - // Extract windows: from previous session + // Extract paths: from previous session else { - windowsToOpen = this.doGetWindowsFromLastSession(); + pathsToOpen = this.doGetPathsFromLastSession(); + if (pathsToOpen.length === 0) { + pathsToOpen.push(Object.create(null)); // add an empty window if we did not have windows to restore + } + restoredWindows = true; } @@ -842,15 +623,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // If we are in `addMode`, we should not do this because in that case all // folders should be added to the existing window. if (!openConfig.addMode && isCommandLineOrAPICall) { - const foldersToOpen = windowsToOpen.filter(path => !!path.folderUri); + const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)) as ISingleFolderWorkspacePathToOpen[]; if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; - if (foldersToOpen.every(f => f.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority - const workspace = this.workspacesMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.folderUri! }))); + if (foldersToOpen.every(folderToOpen => folderToOpen.remoteAuthority === remoteAuthority)) { // only if all folder have the same authority + const workspace = this.workspacesManagementMainService.createUntitledWorkspaceSync(foldersToOpen.map(folder => ({ uri: folder.workspace.uri }))); // Add workspace and remove folders thereby - windowsToOpen.push({ workspace, remoteAuthority }); - windowsToOpen = windowsToOpen.filter(path => !path.folderUri); + pathsToOpen.push({ workspace, remoteAuthority }); + pathsToOpen = pathsToOpen.filter(path => !isSingleFolderWorkspacePathToOpen(path)); } } } @@ -861,63 +642,57 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Use `unshift` to ensure any new window to open comes last // for proper focus treatment. if (openConfig.initialStartup && !restoredWindows && this.configurationService.getValue('window')?.restoreWindows === 'preserve') { - windowsToOpen.unshift(...this.doGetWindowsFromLastSession().filter(window => window.workspace || window.folderUri || window.backupPath)); + pathsToOpen.unshift(...this.doGetPathsFromLastSession().filter(path => isWorkspacePathToOpen(path) || isSingleFolderWorkspacePathToOpen(path) || path.backupPath)); } - return windowsToOpen; + return pathsToOpen; } private doExtractPathsFromAPI(openConfig: IOpenConfiguration): IPathToOpen[] { const pathsToOpen: IPathToOpen[] = []; - const parseOptions: IPathParseOptions = { gotoLineMode: openConfig.gotoLineMode }; - for (const pathToOpen of openConfig.urisToOpen || []) { - if (!pathToOpen) { - continue; - } + const pathResolveOptions: IPathResolveOptions = { gotoLineMode: openConfig.gotoLineMode }; + for (const pathToOpen of coalesce(openConfig.urisToOpen || [])) { + const path = this.resolveOpenable(pathToOpen, pathResolveOptions); - const path = this.parseUri(pathToOpen, parseOptions); + // Path exists if (path) { path.label = pathToOpen.label; pathsToOpen.push(path); - } else { - const uri = this.resourceFromURIToOpen(pathToOpen); + } - // Warn about the invalid URI or path - let message, detail; - if (uri.scheme === Schemas.file) { - message = localize('pathNotExistTitle', "Path does not exist"); - detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)); - } else { - message = localize('uriInvalidTitle', "URI can not be opened"); - detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()); - } + // Path does not exist: show a warning box + else { + const uri = this.resourceFromOpenable(pathToOpen); const options: MessageBoxOptions = { title: product.nameLong, type: 'info', buttons: [localize('ok', "OK")], - message, - detail, + 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('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()), noLink: true }; this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); } } + return pathsToOpen; } private doExtractPathsFromCLI(cli: NativeParsedArgs): IPath[] { const pathsToOpen: IPathToOpen[] = []; - const parseOptions: IPathParseOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined }; + const pathResolveOptions: IPathResolveOptions = { ignoreFileNotFound: true, gotoLineMode: cli.goto, remoteAuthority: cli.remote || undefined }; // folder uris const folderUris = cli['folder-uri']; if (folderUris) { - for (let f of folderUris) { - const folderUri = this.argToUri(f); + for (const rawFolderUri of folderUris) { + const folderUri = this.cliArgToUri(rawFolderUri); if (folderUri) { - const path = this.parseUri({ folderUri }, parseOptions); + const path = this.resolveOpenable({ folderUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -928,10 +703,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // file uris const fileUris = cli['file-uri']; if (fileUris) { - for (let f of fileUris) { - const fileUri = this.argToUri(f); + for (const rawFileUri of fileUris) { + const fileUri = this.cliArgToUri(rawFileUri); if (fileUri) { - const path = this.parseUri(hasWorkspaceFileExtension(f) ? { workspaceUri: fileUri } : { fileUri }, parseOptions); + const path = this.resolveOpenable(hasWorkspaceFileExtension(rawFileUri) ? { workspaceUri: fileUri } : { fileUri }, pathResolveOptions); if (path) { pathsToOpen.push(path); } @@ -940,30 +715,42 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // folder or file paths - const cliArgs = cli._; - for (let cliArg of cliArgs) { - const path = this.parsePath(cliArg, parseOptions); + const cliPaths = cli._; + for (const cliPath of cliPaths) { + const path = this.doResolveFileOpenable(cliPath, pathResolveOptions); if (path) { pathsToOpen.push(path); } } - if (pathsToOpen.length) { - return pathsToOpen; - } - - // No path provided, return empty to open empty - return [Object.create(null)]; + return pathsToOpen; } - private doGetWindowsFromLastSession(): IPathToOpen[] { + private cliArgToUri(arg: string): URI | undefined { + try { + const uri = URI.parse(arg); + if (!uri.scheme) { + this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); + + return undefined; + } + + return uri; + } catch (e) { + this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + } + + return undefined; + } + + private doGetPathsFromLastSession(): IPathToOpen[] { const restoreWindowsSetting = this.getRestoreWindowsSetting(); switch (restoreWindowsSetting) { - // none: we always open an empty window + // none: no window to restore case 'none': - return [Object.create(null)]; + return []; // one: restore last opened workspace/folder or empty window // all: restore all windows @@ -974,48 +761,41 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic case 'folders': // Collect previously opened windows - const openedWindows: IWindowState[] = []; + const lastSessionWindows: IWindowState[] = []; if (restoreWindowsSetting !== 'one') { - openedWindows.push(...this.windowsState.openedWindows); + lastSessionWindows.push(...this.windowsStateHandler.state.openedWindows); } - if (this.windowsState.lastActiveWindow) { - openedWindows.push(this.windowsState.lastActiveWindow); + if (this.windowsStateHandler.state.lastActiveWindow) { + lastSessionWindows.push(this.windowsStateHandler.state.lastActiveWindow); } - const windowsToOpen: IPathToOpen[] = []; - for (const openedWindow of openedWindows) { + const pathsToOpen: IPathToOpen[] = []; + for (const lastSessionWindow of lastSessionWindows) { // Workspaces - if (openedWindow.workspace) { - const pathToOpen = this.parseUri({ workspaceUri: openedWindow.workspace.configPath }, { remoteAuthority: openedWindow.remoteAuthority }); - if (pathToOpen?.workspace) { - windowsToOpen.push(pathToOpen); + if (lastSessionWindow.workspace) { + const pathToOpen = this.resolveOpenable({ workspaceUri: lastSessionWindow.workspace.configPath }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + if (isWorkspacePathToOpen(pathToOpen)) { + pathsToOpen.push(pathToOpen); } } // Folders - else if (openedWindow.folderUri) { - const pathToOpen = this.parseUri({ folderUri: openedWindow.folderUri }, { remoteAuthority: openedWindow.remoteAuthority }); - if (pathToOpen?.folderUri) { - windowsToOpen.push(pathToOpen); + else if (lastSessionWindow.folderUri) { + const pathToOpen = this.resolveOpenable({ folderUri: lastSessionWindow.folderUri }, { remoteAuthority: lastSessionWindow.remoteAuthority }); + if (isSingleFolderWorkspacePathToOpen(pathToOpen)) { + pathsToOpen.push(pathToOpen); } } // Empty window, potentially editors open to be restored - else if (restoreWindowsSetting !== 'folders' && openedWindow.backupPath) { - windowsToOpen.push({ backupPath: openedWindow.backupPath, remoteAuthority: openedWindow.remoteAuthority }); + else if (restoreWindowsSetting !== 'folders' && lastSessionWindow.backupPath) { + pathsToOpen.push({ backupPath: lastSessionWindow.backupPath, remoteAuthority: lastSessionWindow.remoteAuthority }); } } - if (windowsToOpen.length > 0) { - return windowsToOpen; - } - - break; + return pathsToOpen; } - - // Always fallback to empty window - return [Object.create(null)]; } private getRestoreWindowsSetting(): RestoreWindowsSetting { @@ -1034,75 +814,48 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return restoreWindows; } - private argToUri(arg: string): URI | undefined { - try { - const uri = URI.parse(arg); - if (!uri.scheme) { - this.logService.error(`Invalid URI input string, scheme missing: ${arg}`); - return undefined; - } + private resolveOpenable(openable: IWindowOpenable, options: IPathResolveOptions = {}): IPathToOpen | undefined { - return uri; - } catch (e) { - this.logService.error(`Invalid URI input string: ${arg}, ${e.message}`); + // handle file:// openables with some extra validation + let uri = this.resourceFromOpenable(openable); + if (uri.scheme === Schemas.file) { + return this.doResolveFileOpenable(openable, options); } - return undefined; + // handle non file:// openables + return this.doResolveRemoteOpenable(openable, options); } - private parseUri(toOpen: IWindowOpenable, options: IPathParseOptions = {}): IPathToOpen | undefined { - if (!toOpen) { - return undefined; - } - - let uri = this.resourceFromURIToOpen(toOpen); - if (uri.scheme === Schemas.file) { - return this.parsePath(uri.fsPath, options, isFileToOpen(toOpen)); - } + private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let uri = this.resourceFromOpenable(openable); // open remote if either specified in the cli or if it's a remotehost URI const remoteAuthority = options.remoteAuthority || getRemoteAuthority(uri); // normalize URI - uri = normalizePath(uri); - - // remove trailing slash - uri = removeTrailingPathSeparator(uri); + uri = removeTrailingPathSeparator(normalizePath(uri)); // File - if (isFileToOpen(toOpen)) { + if (isFileToOpen(openable)) { if (options.gotoLineMode) { - const parsedPath = parseLineAndColumnAware(uri.path); - return { - fileUri: uri.with({ path: parsedPath.path }), - lineNumber: parsedPath.line, - columnNumber: parsedPath.column, - remoteAuthority - }; + const { path, line, column } = parseLineAndColumnAware(uri.path); + + return { fileUri: uri.with({ path }), lineNumber: line, columnNumber: column, remoteAuthority }; } - return { - fileUri: uri, - remoteAuthority - }; + return { fileUri: uri, remoteAuthority }; } // Workspace - else if (isWorkspaceToOpen(toOpen)) { - return { - workspace: getWorkspaceIdentifier(uri), - remoteAuthority - }; + else if (isWorkspaceToOpen(openable)) { + return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } // Folder - return { - folderUri: uri, - remoteAuthority - }; + return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; } - private resourceFromURIToOpen(openable: IWindowOpenable): URI { + private resourceFromOpenable(openable: IWindowOpenable): URI { if (isWorkspaceToOpen(openable)) { return openable.workspaceUri; } @@ -1114,101 +867,113 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private parsePath(anyPath: string, options: IPathParseOptions, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { - if (!anyPath) { - return undefined; + private doResolveFileOpenable(path: string, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined; + private doResolveFileOpenable(pathOrOpenable: string | IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + let path: string; + let forceOpenWorkspaceAsFile = false; + if (typeof pathOrOpenable === 'string') { + path = pathOrOpenable; + } else { + path = this.resourceFromOpenable(pathOrOpenable).fsPath; + forceOpenWorkspaceAsFile = isFileToOpen(pathOrOpenable); } + // Extract line/col information from path let lineNumber: number | undefined; let columnNumber: number | undefined; if (options.gotoLineMode) { - const parsedPath = parseLineAndColumnAware(anyPath); + const parsedPath = parseLineAndColumnAware(path); lineNumber = parsedPath.line; columnNumber = parsedPath.column; - anyPath = parsedPath.path; + path = parsedPath.path; } - // open remote if either specified in the cli even if it is a local file. + // With remote: resolve path as remote URI const remoteAuthority = options.remoteAuthority; if (remoteAuthority) { - const first = anyPath.charCodeAt(0); - - // make absolute - if (first !== CharCode.Slash) { - if (isWindowsDriveLetter(first) && anyPath.charCodeAt(anyPath.charCodeAt(1)) === CharCode.Colon) { - anyPath = toSlashes(anyPath); - } - - anyPath = `/${anyPath}`; - } - - const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: anyPath }); - - // guess the file type: If it ends with a slash it's a folder. If it has a file extension, it's a file or a workspace. By defaults it's a folder. - if (anyPath.charCodeAt(anyPath.length - 1) !== CharCode.Slash) { - if (hasWorkspaceFileExtension(anyPath)) { - if (forceOpenWorkspaceAsFile) { - return { fileUri: uri, remoteAuthority }; - } - } else if (posix.basename(anyPath).indexOf('.') !== -1) { // file name starts with a dot or has an file extension - return { fileUri: uri, remoteAuthority }; - } - } - - return { folderUri: uri, remoteAuthority }; + return this.doResolvePathRemote(path, remoteAuthority, forceOpenWorkspaceAsFile); } - let candidate = normalize(anyPath); + // Without remote: resolve path as local URI + return this.doResolvePathLocal(path, options, lineNumber, columnNumber, forceOpenWorkspaceAsFile); + } + + private doResolvePathRemote(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + const first = path.charCodeAt(0); + + // make absolute + if (first !== CharCode.Slash) { + if (isWindowsDriveLetter(first) && path.charCodeAt(path.charCodeAt(1)) === CharCode.Colon) { + path = toSlashes(path); + } + + path = `/${path}`; + } + + const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: path }); + + // guess the file type: + // - if it ends with a slash it's a folder + // - if it has a file extension, it's a file or a workspace + // - by defaults it's a folder + if (path.charCodeAt(path.length - 1) !== CharCode.Slash) { + + // file name ends with .code-workspace + if (hasWorkspaceFileExtension(path)) { + if (forceOpenWorkspaceAsFile) { + return { fileUri: uri, remoteAuthority }; + } + return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; + } + + // file name starts with a dot or has an file extension + else if (posix.basename(path).indexOf('.') !== -1) { + return { fileUri: uri, remoteAuthority }; + } + } + + return { workspace: getSingleFolderWorkspaceIdentifier(uri), remoteAuthority }; + } + + private doResolvePathLocal(path: string, options: IPathResolveOptions, lineNumber: number | undefined, columnNumber: number | undefined, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined { + + // Ensure the path is normalized and absolute + path = sanitizeFilePath(normalize(path), process.env['VSCODE_CWD'] || process.cwd()); try { - const candidateStat = fs.statSync(candidate); - if (candidateStat.isFile()) { + const pathStat = statSync(path); + if (pathStat.isFile()) { // Workspace (unless disabled via flag) if (!forceOpenWorkspaceAsFile) { - const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate)); + const workspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(URI.file(path)); if (workspace) { - return { - workspace: { id: workspace.id, configPath: workspace.configPath }, - remoteAuthority: workspace.remoteAuthority, - exists: true - }; + return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority, exists: true }; } } // File - return { - fileUri: URI.file(candidate), - lineNumber, - columnNumber, - remoteAuthority, - exists: true - }; + return { fileUri: URI.file(path), lineNumber, columnNumber, exists: true }; } // Folder (we check for isDirectory() because e.g. paths like /dev/null // are neither file nor folder but some external tools might pass them // over to us) - else if (candidateStat.isDirectory()) { - return { - folderUri: URI.file(candidate), - remoteAuthority, - exists: true - }; + else if (pathStat.isDirectory()) { + return { workspace: getSingleFolderWorkspaceIdentifier(URI.file(path), pathStat), exists: true }; } } catch (error) { - const fileUri = URI.file(candidate); - this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent + const fileUri = URI.file(path); + + // since file does not seem to exist anymore, remove from recent + this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // assume this is a file that does not yet exist if (options?.ignoreFileNotFound) { - return { - fileUri, - remoteAuthority, - exists: false - }; + return { fileUri, exists: false }; } } @@ -1257,12 +1022,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { openFolderInNewWindow: !!openFolderInNewWindow, openFilesInNewWindow }; } - openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[] { + openExtensionDevelopmentHostWindow(extensionDevelopmentPaths: string[], openConfig: IOpenConfiguration): ICodeWindow[] { // Reload an existing extension development host window on the same path // We currently do not allow more than one extension development window // on the same extension path. - const existingWindow = findWindowOnExtensionDevelopmentPath(WindowsMainService.WINDOWS, extensionDevelopmentPath); + const existingWindow = findWindowOnExtensionDevelopmentPath(this.getWindows(), extensionDevelopmentPaths); if (existingWindow) { this.lifecycleMainService.reload(existingWindow, openConfig.cli); existingWindow.focus(); // make sure it gets focus and is restored @@ -1276,10 +1041,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Fill in previously opened workspace unless an explicit path is provided and we are not unit testing if (!cliArgs.length && !folderUris.length && !fileUris.length && !openConfig.cli.extensionTestsPath) { - const extensionDevelopmentWindowState = this.windowsState.lastPluginDevelopmentHostWindow; + const extensionDevelopmentWindowState = this.windowsStateHandler.state.lastPluginDevelopmentHostWindow; const workspaceToOpen = extensionDevelopmentWindowState && (extensionDevelopmentWindowState.workspace || extensionDevelopmentWindowState.folderUri); if (workspaceToOpen) { - if (isSingleFolderWorkspaceIdentifier(workspaceToOpen)) { + if (URI.isUri(workspaceToOpen)) { if (workspaceToOpen.scheme === Schemas.file) { cliArgs = [workspaceToOpen.fsPath]; } else { @@ -1296,9 +1061,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } let authority = ''; - for (let p of extensionDevelopmentPath) { - if (p.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { - const url = URI.parse(p); + for (const extensionDevelopmentPath of extensionDevelopmentPaths) { + if (extensionDevelopmentPath.match(/^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/)) { + const url = URI.parse(extensionDevelopmentPath); if (url.scheme === Schemas.vscodeRemote) { if (authority) { if (url.authority !== authority) { @@ -1317,7 +1082,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic cliArgs = cliArgs.filter(path => { const uri = URI.file(path); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, uri)) { + if (!!findWindowOnWorkspaceOrFolder(this.getWindows(), uri)) { return false; } @@ -1325,8 +1090,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); folderUris = folderUris.filter(folderUriStr => { - const folderUri = this.argToUri(folderUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, folderUri)) { + const folderUri = this.cliArgToUri(folderUriStr); + if (folderUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), folderUri)) { return false; } @@ -1334,8 +1099,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }); fileUris = fileUris.filter(fileUriStr => { - const fileUri = this.argToUri(fileUriStr); - if (!!findWindowOnWorkspaceOrFolderUri(WindowsMainService.WINDOWS, fileUri)) { + const fileUri = this.cliArgToUri(fileUriStr); + if (fileUri && !!findWindowOnWorkspaceOrFolder(this.getWindows(), fileUri)) { return false; } @@ -1368,7 +1133,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { - // Build INativeWindowConfiguration from config and options + // Build `INativeWindowConfiguration` from config and options const configuration = { ...options.cli } as INativeWindowConfiguration; configuration.appRoot = this.environmentService.appRoot; configuration.machineId = this.machineId; @@ -1378,7 +1143,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic configuration.userEnv = { ...this.initialUserEnv, ...options.userEnv }; configuration.isInitialStartup = options.initialStartup; configuration.workspace = options.workspace; - configuration.folderUri = options.folderUri; configuration.remoteAuthority = options.remoteAuthority; const filesToOpen = options.filesToOpen; @@ -1406,32 +1170,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // New window if (!window) { - const windowConfig = this.configurationService.getValue('window'); - const state = this.getNewWindowState(configuration); - - // Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen - let allowFullscreen: boolean; - if (state.hasDefaultState) { - allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); - } - - // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore - else { - allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen); - - if (allowFullscreen && isMacintosh && WindowsMainService.WINDOWS.some(win => win.isFullScreen)) { - // macOS: Electron does not allow to restore multiple windows in - // fullscreen. As such, if we already restored a window in that - // state, we cannot allow more fullscreen windows. See - // https://github.com/microsoft/vscode/issues/41691 and - // https://github.com/electron/electron/issues/13077 - allowFullscreen = false; - } - } - - if (state.mode === WindowMode.Fullscreen && !allowFullscreen) { - state.mode = WindowMode.Normal; - } + const state = this.windowsStateHandler.getNewWindowState(configuration); // Create the window const createdWindow = window = this.instantiationService.createInstance(CodeWindow, { @@ -1455,12 +1194,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic this._onWindowOpened.fire(createdWindow); // Indicate number change via event - this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length - 1, newCount: WindowsMainService.WINDOWS.length }); + this._onWindowsCountChanged.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.onBeforeWindowClose(createdWindow)); // try to save state before destroy because close will not fire + 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)); @@ -1504,10 +1243,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Register window for backups if (!configuration.extensionDevelopmentPath) { - if (configuration.workspace) { + if (isWorkspaceIdentifier(configuration.workspace)) { configuration.backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace: configuration.workspace, remoteAuthority: configuration.remoteAuthority }); - } else if (configuration.folderUri) { - configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.folderUri); + } else if (isSingleFolderWorkspaceIdentifier(configuration.workspace)) { + configuration.backupPath = this.backupMainService.registerFolderBackupSync(configuration.workspace.uri); } else { const backupFolder = options.emptyWindowBackupInfo && options.emptyWindowBackupInfo.backupFolder; configuration.backupPath = this.backupMainService.registerEmptyWindowBackupSync(backupFolder, configuration.remoteAuthority); @@ -1518,162 +1257,37 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.load(configuration); } - private getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { - const lastActive = this.getLastActiveWindow(); - - // Restore state unless we are running extension tests - if (!configuration.extensionTestsPath) { - - // extension development host Window - load from stored settings if any - if (!!configuration.extensionDevelopmentPath && this.windowsState.lastPluginDevelopmentHostWindow) { - return this.windowsState.lastPluginDevelopmentHostWindow.uiState; - } - - // Known Workspace - load from stored settings - const workspace = configuration.workspace; - if (workspace) { - const stateForWorkspace = this.windowsState.openedWindows.filter(o => o.workspace && o.workspace.id === workspace.id).map(o => o.uiState); - if (stateForWorkspace.length) { - return stateForWorkspace[0]; - } - } - - // Known Folder - load from stored settings - if (configuration.folderUri) { - const stateForFolder = this.windowsState.openedWindows.filter(o => o.folderUri && extUriBiasedIgnorePathCase.isEqual(o.folderUri, configuration.folderUri)).map(o => o.uiState); - if (stateForFolder.length) { - return stateForFolder[0]; - } - } - - // Empty windows with backups - else if (configuration.backupPath) { - const stateForEmptyWindow = this.windowsState.openedWindows.filter(o => o.backupPath === configuration.backupPath).map(o => o.uiState); - if (stateForEmptyWindow.length) { - return stateForEmptyWindow[0]; - } - } - - // First Window - const lastActiveState = this.lastClosedWindowState || this.windowsState.lastActiveWindow; - if (!lastActive && lastActiveState) { - return lastActiveState.uiState; - } - } - - // - // In any other case, we do not have any stored settings for the window state, so we come up with something smart - // - - // We want the new window to open on the same display that the last active one is in - let displayToUse: Display | undefined; - const displays = screen.getAllDisplays(); - - // Single Display - if (displays.length === 1) { - displayToUse = displays[0]; - } - - // Multi Display - else { - - // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is - if (isMacintosh) { - const cursorPoint = screen.getCursorScreenPoint(); - displayToUse = screen.getDisplayNearestPoint(cursorPoint); - } - - // if we have a last active window, use that display for the new window - if (!displayToUse && lastActive) { - displayToUse = screen.getDisplayMatching(lastActive.getBounds()); - } - - // fallback to primary display or first display - if (!displayToUse) { - displayToUse = screen.getPrimaryDisplay() || displays[0]; - } - } - - // Compute x/y based on display bounds - // Note: important to use Math.round() because Electron does not seem to be too happy about - // display coordinates that are not absolute numbers. - let state = defaultWindowState(); - state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); - state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); - - // Check for newWindowDimensions setting and adjust accordingly - const windowConfig = this.configurationService.getValue('window'); - let ensureNoOverlap = true; - if (windowConfig?.newWindowDimensions) { - if (windowConfig.newWindowDimensions === 'maximized') { - state.mode = WindowMode.Maximized; - ensureNoOverlap = false; - } else if (windowConfig.newWindowDimensions === 'fullscreen') { - state.mode = WindowMode.Fullscreen; - ensureNoOverlap = false; - } else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) { - const lastActiveState = lastActive.serializeWindowState(); - if (lastActiveState.mode === WindowMode.Fullscreen) { - state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331) - } else { - state = lastActiveState; - } - - ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset'; - } - } - - if (ensureNoOverlap) { - state = this.ensureNoOverlap(state); - } - - (state as INewWindowState).hasDefaultState = true; // flag as default state - - return state; - } - - private ensureNoOverlap(state: ISingleWindowState): ISingleWindowState { - if (WindowsMainService.WINDOWS.length === 0) { - return state; - } - - state.x = typeof state.x === 'number' ? state.x : 0; - state.y = typeof state.y === 'number' ? state.y : 0; - - const existingWindowBounds = WindowsMainService.WINDOWS.map(win => win.getBounds()); - while (existingWindowBounds.some(b => b.x === state.x || b.y === state.y)) { - state.x += 30; - state.y += 30; - } - - return state; - } - - private onWindowClosed(win: ICodeWindow): void { + private onWindowClosed(window: ICodeWindow): void { // Remove from our list so that Electron can clean it up - const index = WindowsMainService.WINDOWS.indexOf(win); + const index = WindowsMainService.WINDOWS.indexOf(window); WindowsMainService.WINDOWS.splice(index, 1); // Emit - this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length + 1, newCount: WindowsMainService.WINDOWS.length }); + this._onWindowsCountChanged.fire({ oldCount: this.getWindowCount() + 1, newCount: this.getWindowCount() }); } getFocusedWindow(): ICodeWindow | undefined { - const win = BrowserWindow.getFocusedWindow(); - if (win) { - return this.getWindowById(win.id); + const window = BrowserWindow.getFocusedWindow(); + if (window) { + return this.getWindowById(window.id); } return undefined; } getLastActiveWindow(): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS); + return this.doGetLastActiveWindow(this.getWindows()); } private getLastActiveWindowForAuthority(remoteAuthority: string | undefined): ICodeWindow | undefined { - return getLastActiveWindow(WindowsMainService.WINDOWS.filter(window => window.remoteAuthority === remoteAuthority)); + return this.doGetLastActiveWindow(this.getWindows().filter(window => window.remoteAuthority === remoteAuthority)); + } + + private doGetLastActiveWindow(windows: ICodeWindow[]): ICodeWindow | undefined { + const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); + + return windows.find(window => window.lastFocusTime === lastFocusedDate); } sendToFocused(channel: string, ...args: any[]): void { @@ -1685,7 +1299,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void { - for (const window of WindowsMainService.WINDOWS) { + for (const window of this.getWindows()) { if (windowIdsToIgnore && windowIdsToIgnore.indexOf(window.id) >= 0) { continue; // do not send if we are instructed to ignore it } @@ -1694,10 +1308,18 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } - getWindowById(windowId: number): ICodeWindow | undefined { - const res = WindowsMainService.WINDOWS.filter(window => window.id === windowId); + getWindows(): ICodeWindow[] { + return WindowsMainService.WINDOWS; + } - return arrays.firstOrDefault(res); + getWindowCount(): number { + return WindowsMainService.WINDOWS.length; + } + + getWindowById(windowId: number): ICodeWindow | undefined { + const windows = this.getWindows().filter(window => window.id === windowId); + + return firstOrDefault(windows); } getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined { @@ -1708,12 +1330,4 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.getWindowById(browserWindow.id); } - - getWindows(): ICodeWindow[] { - return WindowsMainService.WINDOWS; - } - - getWindowCount(): number { - return WindowsMainService.WINDOWS.length; - } } diff --git a/src/vs/platform/windows/electron-main/windowsStateHandler.ts b/src/vs/platform/windows/electron-main/windowsStateHandler.ts new file mode 100644 index 0000000000..8848e9e914 --- /dev/null +++ b/src/vs/platform/windows/electron-main/windowsStateHandler.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { app, Display, screen } from 'electron'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isMacintosh } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { defaultWindowState } from 'vs/code/electron-main/window'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStateService } from 'vs/platform/state/node/state'; +import { INativeWindowConfiguration, IWindowSettings } from 'vs/platform/windows/common/windows'; +import { ICodeWindow, IWindowsMainService, IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +export interface IWindowState { + workspace?: IWorkspaceIdentifier; + folderUri?: URI; + backupPath?: string; + remoteAuthority?: string; + uiState: IWindowUIState; +} + +export interface IWindowsState { + lastActiveWindow?: IWindowState; + lastPluginDevelopmentHostWindow?: IWindowState; + openedWindows: IWindowState[]; +} + +interface INewWindowState extends IWindowUIState { + hasDefaultState?: boolean; +} + +interface ISerializedWindowsState { + readonly lastActiveWindow?: ISerializedWindowState; + readonly lastPluginDevelopmentHostWindow?: ISerializedWindowState; + readonly openedWindows: ISerializedWindowState[]; +} + +interface ISerializedWindowState { + readonly workspaceIdentifier?: { id: string; configURIPath: string }; + readonly folder?: string; + readonly backupPath?: string; + readonly remoteAuthority?: string; + readonly uiState: IWindowUIState; +} + +export class WindowsStateHandler extends Disposable { + + private static readonly windowsStateStorageKey = 'windowsState'; + + get state() { return this._state; } + private readonly _state = restoreWindowsState(this.stateService.getItem(WindowsStateHandler.windowsStateStorageKey)); + + private lastClosedState: IWindowState | undefined = undefined; + + private shuttingDown = false; + + constructor( + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @IStateService private readonly stateService: IStateService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // When a window looses focus, save all windows state. This allows to + // prevent loss of window-state data when OS is restarted without properly + // shutting down the application (https://github.com/microsoft/vscode/issues/87171) + app.on('browser-window-blur', () => { + if (!this.shuttingDown) { + this.saveWindowsState(); + } + }); + + // Handle various lifecycle events around windows + this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); + this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); + this.windowsMainService.onWindowsCountChanged(e => { + if (e.newCount - e.oldCount > 0) { + // clear last closed window state when a new window opens. this helps on macOS where + // otherwise closing the last window, opening a new window and then quitting would + // use the state of the previously closed window when restarting. + this.lastClosedState = undefined; + } + }); + + // try to save state before destroy because close will not fire + this.windowsMainService.onWindowDestroyed(window => this.onBeforeWindowClose(window)); + } + + // Note that onBeforeShutdown() and onBeforeWindowClose() 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 + // - 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() + // and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown() + // and then onBeforeWindowClose(). + // + // Here is the behavior on different OS depending on action taken (Electron 1.7.x): + // + // Legend + // - quit(N): quit application with N windows opened + // - 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 + // + // macOS + // - quit(1): onBeforeShutdown(1), onBeforeWindowClose(1, true) + // - quit(2): onBeforeShutdown(2), onBeforeWindowClose(2, true), onBeforeWindowClose(2, true) + // - quit(0): onBeforeShutdown(0) + // - close(1): onBeforeWindowClose(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) + // + // 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) + // + private onBeforeShutdown(): void { + this.shuttingDown = true; + + this.saveWindowsState(); + } + + private saveWindowsState(): void { + const currentWindowsState: IWindowsState = { + openedWindows: [], + lastPluginDevelopmentHostWindow: this._state.lastPluginDevelopmentHostWindow, + lastActiveWindow: this.lastClosedState + }; + + // 1.) Find a last active window (pick any other first window otherwise) + if (!currentWindowsState.lastActiveWindow) { + let activeWindow = this.windowsMainService.getLastActiveWindow(); + if (!activeWindow || activeWindow.isExtensionDevelopmentHost) { + activeWindow = this.windowsMainService.getWindows().find(window => !window.isExtensionDevelopmentHost); + } + + if (activeWindow) { + currentWindowsState.lastActiveWindow = this.toWindowState(activeWindow); + } + } + + // 2.) Find extension host window + const extensionHostWindow = this.windowsMainService.getWindows().find(window => window.isExtensionDevelopmentHost && !window.isExtensionTestHost); + if (extensionHostWindow) { + currentWindowsState.lastPluginDevelopmentHostWindow = this.toWindowState(extensionHostWindow); + } + + // 3.) All windows (except extension host) for N >= 2 to support `restoreWindows: all` or for auto update + // + // Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0) + // so if we ever want to persist the UI state of the last closed window (window count === 1), it has + // to come from the stored lastClosedWindowState on Win/Linux at least + if (this.windowsMainService.getWindowCount() > 1) { + currentWindowsState.openedWindows = this.windowsMainService.getWindows().filter(window => !window.isExtensionDevelopmentHost).map(window => this.toWindowState(window)); + } + + // Persist + const state = getWindowsStateStoreData(currentWindowsState); + this.stateService.setItem(WindowsStateHandler.windowsStateStorageKey, state); + + if (this.shuttingDown) { + this.logService.trace('[WindowsStateHandler] onBeforeShutdown', state); + } + } + + // See note on #onBeforeShutdown() for details how these events are flowing + private onBeforeWindowClose(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 + } + + // On Window close, update our stored UI state of this window + const state: IWindowState = this.toWindowState(window); + if (window.isExtensionDevelopmentHost && !window.isExtensionTestHost) { + this._state.lastPluginDevelopmentHostWindow = state; // do not let test run window state overwrite our extension development state + } + + // Any non extension host window with same workspace or folder + else if (!window.isExtensionDevelopmentHost && window.openedWorkspace) { + this._state.openedWindows.forEach(openedWindow => { + const sameWorkspace = isWorkspaceIdentifier(window.openedWorkspace) && openedWindow.workspace?.id === window.openedWorkspace.id; + const sameFolder = isSingleFolderWorkspaceIdentifier(window.openedWorkspace) && openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, window.openedWorkspace.uri); + + if (sameWorkspace || sameFolder) { + openedWindow.uiState = state.uiState; + } + }); + } + + // On Windows and Linux closing the last window will trigger quit. Since we are storing all UI state + // before quitting, we need to remember the UI state of this window to be able to persist it. + // On macOS we keep the last closed window state ready in case the user wants to quit right after or + // wants to open another window, in which case we use this state over the persisted one. + if (this.windowsMainService.getWindowCount() === 1) { + this.lastClosedState = state; + } + } + + private toWindowState(window: ICodeWindow): IWindowState { + return { + workspace: isWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace : undefined, + folderUri: isSingleFolderWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace.uri : undefined, + backupPath: window.backupPath, + remoteAuthority: window.remoteAuthority, + uiState: window.serializeWindowState() + }; + } + + getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { + const state = this.doGetNewWindowState(configuration); + const windowConfig = this.configurationService.getValue('window'); + + // Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen + let allowFullscreen: boolean; + if (state.hasDefaultState) { + allowFullscreen = !!(windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0); + } + + // Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore + else { + allowFullscreen = !!(this.lifecycleMainService.wasRestarted || windowConfig?.restoreFullscreen); + + if (allowFullscreen && isMacintosh && this.windowsMainService.getWindows().some(window => window.isFullScreen)) { + // macOS: Electron does not allow to restore multiple windows in + // fullscreen. As such, if we already restored a window in that + // state, we cannot allow more fullscreen windows. See + // https://github.com/microsoft/vscode/issues/41691 and + // https://github.com/electron/electron/issues/13077 + allowFullscreen = false; + } + } + + if (state.mode === WindowMode.Fullscreen && !allowFullscreen) { + state.mode = WindowMode.Normal; + } + + return state; + } + + private doGetNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { + const lastActive = this.windowsMainService.getLastActiveWindow(); + + // Restore state unless we are running extension tests + if (!configuration.extensionTestsPath) { + + // extension development host Window - load from stored settings if any + if (!!configuration.extensionDevelopmentPath && this.state.lastPluginDevelopmentHostWindow) { + return this.state.lastPluginDevelopmentHostWindow.uiState; + } + + // Known Workspace - load from stored settings + const workspace = configuration.workspace; + if (isWorkspaceIdentifier(workspace)) { + const stateForWorkspace = this.state.openedWindows.filter(openedWindow => openedWindow.workspace && openedWindow.workspace.id === workspace.id).map(openedWindow => openedWindow.uiState); + if (stateForWorkspace.length) { + return stateForWorkspace[0]; + } + } + + // Known Folder - load from stored settings + if (isSingleFolderWorkspaceIdentifier(workspace)) { + const stateForFolder = this.state.openedWindows.filter(openedWindow => openedWindow.folderUri && extUriBiasedIgnorePathCase.isEqual(openedWindow.folderUri, workspace.uri)).map(openedWindow => openedWindow.uiState); + if (stateForFolder.length) { + return stateForFolder[0]; + } + } + + // Empty windows with backups + else if (configuration.backupPath) { + const stateForEmptyWindow = this.state.openedWindows.filter(openedWindow => openedWindow.backupPath === configuration.backupPath).map(openedWindow => openedWindow.uiState); + if (stateForEmptyWindow.length) { + return stateForEmptyWindow[0]; + } + } + + // First Window + const lastActiveState = this.lastClosedState || this.state.lastActiveWindow; + if (!lastActive && lastActiveState) { + return lastActiveState.uiState; + } + } + + // + // In any other case, we do not have any stored settings for the window state, so we come up with something smart + // + + // We want the new window to open on the same display that the last active one is in + let displayToUse: Display | undefined; + const displays = screen.getAllDisplays(); + + // Single Display + if (displays.length === 1) { + displayToUse = displays[0]; + } + + // Multi Display + else { + + // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is + if (isMacintosh) { + const cursorPoint = screen.getCursorScreenPoint(); + displayToUse = screen.getDisplayNearestPoint(cursorPoint); + } + + // if we have a last active window, use that display for the new window + if (!displayToUse && lastActive) { + displayToUse = screen.getDisplayMatching(lastActive.getBounds()); + } + + // fallback to primary display or first display + if (!displayToUse) { + displayToUse = screen.getPrimaryDisplay() || displays[0]; + } + } + + // Compute x/y based on display bounds + // Note: important to use Math.round() because Electron does not seem to be too happy about + // display coordinates that are not absolute numbers. + let state = defaultWindowState(); + state.x = Math.round(displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (state.width! / 2)); + state.y = Math.round(displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (state.height! / 2)); + + // Check for newWindowDimensions setting and adjust accordingly + const windowConfig = this.configurationService.getValue('window'); + let ensureNoOverlap = true; + if (windowConfig?.newWindowDimensions) { + if (windowConfig.newWindowDimensions === 'maximized') { + state.mode = WindowMode.Maximized; + ensureNoOverlap = false; + } else if (windowConfig.newWindowDimensions === 'fullscreen') { + state.mode = WindowMode.Fullscreen; + ensureNoOverlap = false; + } else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) { + const lastActiveState = lastActive.serializeWindowState(); + if (lastActiveState.mode === WindowMode.Fullscreen) { + state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/microsoft/vscode/issues/19331) + } else { + state = lastActiveState; + } + + ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset'; + } + } + + if (ensureNoOverlap) { + state = this.ensureNoOverlap(state); + } + + (state as INewWindowState).hasDefaultState = true; // flag as default state + + return state; + } + + private ensureNoOverlap(state: IWindowUIState): IWindowUIState { + if (this.windowsMainService.getWindows().length === 0) { + return state; + } + + state.x = typeof state.x === 'number' ? state.x : 0; + state.y = typeof state.y === 'number' ? state.y : 0; + + const existingWindowBounds = this.windowsMainService.getWindows().map(window => window.getBounds()); + while (existingWindowBounds.some(bounds => bounds.x === state.x || bounds.y === state.y)) { + state.x += 30; + state.y += 30; + } + + return state; + } +} + +export function restoreWindowsState(data: ISerializedWindowsState | undefined): IWindowsState { + const result: IWindowsState = { openedWindows: [] }; + const windowsState = data || { openedWindows: [] }; + + if (windowsState.lastActiveWindow) { + result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); + } + + if (windowsState.lastPluginDevelopmentHostWindow) { + result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); + } + + if (Array.isArray(windowsState.openedWindows)) { + result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); + } + + return result; +} + +function restoreWindowState(windowState: ISerializedWindowState): IWindowState { + const result: IWindowState = { uiState: windowState.uiState }; + if (windowState.backupPath) { + result.backupPath = windowState.backupPath; + } + + if (windowState.remoteAuthority) { + result.remoteAuthority = windowState.remoteAuthority; + } + + if (windowState.folder) { + result.folderUri = URI.parse(windowState.folder); + } + + if (windowState.workspaceIdentifier) { + result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; + } + + return result; +} + +export function getWindowsStateStoreData(windowsState: IWindowsState): IWindowsState { + return { + lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), + lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), + openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) + }; +} + +function serializeWindowState(windowState: IWindowState): ISerializedWindowState { + return { + workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, + folder: windowState.folderUri && windowState.folderUri.toString(), + backupPath: windowState.backupPath, + remoteAuthority: windowState.remoteAuthority, + uiState: windowState.uiState + }; +} diff --git a/src/vs/platform/windows/electron-main/windowsStateStorage.ts b/src/vs/platform/windows/electron-main/windowsStateStorage.ts deleted file mode 100644 index 992f6f2a09..0000000000 --- a/src/vs/platform/windows/electron-main/windowsStateStorage.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI, UriComponents } from 'vs/base/common/uri'; -import { IWindowState as IWindowUIState } from 'vs/platform/windows/electron-main/windows'; -import { IWindowState, IWindowsState } from 'vs/platform/windows/electron-main/windowsMainService'; - -export type WindowsStateStorageData = object; - -interface ISerializedWindowsState { - lastActiveWindow?: ISerializedWindowState; - lastPluginDevelopmentHostWindow?: ISerializedWindowState; - openedWindows: ISerializedWindowState[]; -} - -interface ISerializedWindowState { - workspaceIdentifier?: { id: string; configURIPath: string }; - folder?: string; - backupPath?: string; - remoteAuthority?: string; - uiState: IWindowUIState; - - // deprecated - folderUri?: UriComponents; - folderPath?: string; - workspace?: { id: string; configPath: string }; -} - -export function restoreWindowsState(data: WindowsStateStorageData | undefined): IWindowsState { - const result: IWindowsState = { openedWindows: [] }; - const windowsState = data as ISerializedWindowsState || { openedWindows: [] }; - - if (windowsState.lastActiveWindow) { - result.lastActiveWindow = restoreWindowState(windowsState.lastActiveWindow); - } - - if (windowsState.lastPluginDevelopmentHostWindow) { - result.lastPluginDevelopmentHostWindow = restoreWindowState(windowsState.lastPluginDevelopmentHostWindow); - } - - if (Array.isArray(windowsState.openedWindows)) { - result.openedWindows = windowsState.openedWindows.map(windowState => restoreWindowState(windowState)); - } - - return result; -} - -function restoreWindowState(windowState: ISerializedWindowState): IWindowState { - const result: IWindowState = { uiState: windowState.uiState }; - if (windowState.backupPath) { - result.backupPath = windowState.backupPath; - } - - if (windowState.remoteAuthority) { - result.remoteAuthority = windowState.remoteAuthority; - } - - if (windowState.folder) { - result.folderUri = URI.parse(windowState.folder); - } else if (windowState.folderUri) { - result.folderUri = URI.revive(windowState.folderUri); - } else if (windowState.folderPath) { - result.folderUri = URI.file(windowState.folderPath); - } - - if (windowState.workspaceIdentifier) { - result.workspace = { id: windowState.workspaceIdentifier.id, configPath: URI.parse(windowState.workspaceIdentifier.configURIPath) }; - } else if (windowState.workspace) { - result.workspace = { id: windowState.workspace.id, configPath: URI.file(windowState.workspace.configPath) }; - } - - return result; -} - -export function getWindowsStateStoreData(windowsState: IWindowsState): WindowsStateStorageData { - return { - lastActiveWindow: windowsState.lastActiveWindow && serializeWindowState(windowsState.lastActiveWindow), - lastPluginDevelopmentHostWindow: windowsState.lastPluginDevelopmentHostWindow && serializeWindowState(windowsState.lastPluginDevelopmentHostWindow), - openedWindows: windowsState.openedWindows.map(ws => serializeWindowState(ws)) - }; -} - -function serializeWindowState(windowState: IWindowState): ISerializedWindowState { - return { - workspaceIdentifier: windowState.workspace && { id: windowState.workspace.id, configURIPath: windowState.workspace.configPath.toString() }, - folder: windowState.folderUri && windowState.folderUri.toString(), - backupPath: windowState.backupPath, - remoteAuthority: windowState.remoteAuthority, - uiState: windowState.uiState - }; -} diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts deleted file mode 100644 index 6fb8771c4e..0000000000 --- a/src/vs/platform/windows/node/window.ts +++ /dev/null @@ -1,150 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vs/base/common/uri'; -import * as platform from 'vs/base/common/platform'; -import * as extpath from 'vs/base/common/extpath'; -import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; - -export const enum OpenContext { - - // opening when running from the command line - CLI, - - // macOS only: opening from the dock (also when opening files to a running instance from desktop) - DOCK, - - // opening from the main application window - MENU, - - // opening from a file or folder dialog - DIALOG, - - // opening from the OS's UI - DESKTOP, - - // opening through the API - API -} - -export interface IWindowContext { - openedWorkspace?: IWorkspaceIdentifier; - openedFolderUri?: URI; - - extensionDevelopmentPath?: string[]; - lastFocusTime: number; -} - -export interface IBestWindowOrFolderOptions { - windows: W[]; - newWindow: boolean; - context: OpenContext; - fileUri?: URI; - codeSettingsFolder?: string; - localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null; -} - -export function findBestWindowOrFolderForFile({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions): W | undefined { - if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) { - const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver); - if (windowOnFilePath) { - return windowOnFilePath; - } - } - return !newWindow ? getLastActiveWindow(windows) : undefined; -} - -function findWindowOnFilePath(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null { - - // First check for windows with workspaces that have a parent folder of the provided path opened - for (const window of windows) { - const workspace = window.openedWorkspace; - if (workspace) { - const resolvedWorkspace = localWorkspaceResolver(workspace); - if (resolvedWorkspace) { - // workspace could be resolved: It's in the local file system - if (resolvedWorkspace.folders.some(folder => extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, folder.uri))) { - return window; - } - } else { - // use the config path instead - if (extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, workspace.configPath)) { - return window; - } - } - } - } - - // Then go with single folder windows that are parent of the provided file path - const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && extUriBiasedIgnorePathCase.isEqualOrParent(fileUri, window.openedFolderUri)); - if (singleFolderWindowsOnFilePath.length) { - return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri!.path.length - b.openedFolderUri!.path.length))[0]; - } - - return null; -} - -export function getLastActiveWindow(windows: W[]): W | undefined { - const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime)); - - return windows.find(window => window.lastFocusTime === lastFocusedDate); -} - -export function findWindowOnWorkspace(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W | null { - if (isSingleFolderWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on folder - if (isSingleFolderWorkspaceIdentifier(workspace)) { - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, workspace)) { - return window; - } - } - } - } else if (isWorkspaceIdentifier(workspace)) { - for (const window of windows) { - // match on workspace - if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) { - return window; - } - } - } - return null; -} - -export function findWindowOnExtensionDevelopmentPath(windows: W[], extensionDevelopmentPaths: string[]): W | null { - - const matches = (uriString: string): boolean => { - return extensionDevelopmentPaths.some(p => extpath.isEqual(p, uriString, !platform.isLinux /* ignorecase */)); - }; - - for (const window of windows) { - // match on extension development path. The path can be one or more paths or uri strings, using paths.isEqual is not 100% correct but good enough - const currPaths = window.extensionDevelopmentPath; - if (currPaths?.some(p => matches(p))) { - return window; - } - } - - return null; -} - -export function findWindowOnWorkspaceOrFolderUri(windows: W[], uri: URI | undefined): W | null { - if (!uri) { - return null; - } - for (const window of windows) { - // check for workspace config path - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, uri)) { - return window; - } - - // check for folder path - if (window.openedFolderUri && extUriBiasedIgnorePathCase.isEqual(window.openedFolderUri, uri)) { - return window; - } - } - return null; -} diff --git a/src/vs/platform/windows/common/windowTracker.ts b/src/vs/platform/windows/node/windowTracker.ts similarity index 100% rename from src/vs/platform/windows/common/windowTracker.ts rename to src/vs/platform/windows/node/windowTracker.ts diff --git a/src/vs/platform/windows/test/electron-main/window.test.ts b/src/vs/platform/windows/test/electron-main/window.test.ts new file mode 100644 index 0000000000..e64fda1ffb --- /dev/null +++ b/src/vs/platform/windows/test/electron-main/window.test.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { IWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { UriDto } from 'vs/base/common/types'; +import { ICommandAction } from 'vs/platform/actions/common/actions'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; + +suite('WindowsFinder', () => { + + const fixturesFolder = getPathFromAmdModule(require, './fixtures'); + + const testWorkspace: IWorkspaceIdentifier = { + id: Date.now().toString(), + configPath: URI.file(join(fixturesFolder, 'workspaces.json')) + }; + + const testWorkspaceFolders = toWorkspaceFolders([{ path: join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); + const localWorkspaceResolver = (workspace: any) => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }; + + 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; + whenClosedOrLoaded: Promise = Promise.resolve(); + id: number = -1; + win: Electron.BrowserWindow = undefined!; + config: INativeWindowConfiguration | undefined; + openedWorkspace = options.openedFolderUri ? { id: '', uri: options.openedFolderUri } : options.openedWorkspace; + backupPath?: string | undefined; + remoteAuthority?: string | undefined; + isExtensionDevelopmentHost = false; + isExtensionTestHost = false; + lastFocusTime = options.lastFocusTime; + isFullScreen = false; + isReady = true; + hasHiddenTitleBarStyle = false; + + ready(): Promise { throw new Error('Method not implemented.'); } + setReady(): void { throw new Error('Method not implemented.'); } + addTabbedWindow(window: ICodeWindow): void { throw new Error('Method not implemented.'); } + load(config: INativeWindowConfiguration, options: { isReload?: boolean }): void { throw new Error('Method not implemented.'); } + reload(cli?: NativeParsedArgs): void { throw new Error('Method not implemented.'); } + focus(options?: { force: boolean; }): void { throw new Error('Method not implemented.'); } + close(): void { throw new Error('Method not implemented.'); } + getBounds(): Electron.Rectangle { throw new Error('Method not implemented.'); } + send(channel: string, ...args: any[]): void { throw new Error('Method not implemented.'); } + sendWhenReady(channel: string, token: CancellationToken, ...args: any[]): void { throw new Error('Method not implemented.'); } + toggleFullScreen(): void { throw new Error('Method not implemented.'); } + isMinimized(): boolean { throw new Error('Method not implemented.'); } + setRepresentedFilename(name: string): void { throw new Error('Method not implemented.'); } + getRepresentedFilename(): string | undefined { throw new Error('Method not implemented.'); } + setDocumentEdited(edited: boolean): void { throw new Error('Method not implemented.'); } + isDocumentEdited(): boolean { throw new Error('Method not implemented.'); } + handleTitleDoubleClick(): void { throw new Error('Method not implemented.'); } + updateTouchBar(items: UriDto[][]): void { throw new Error('Method not implemented.'); } + serializeWindowState(): IWindowState { throw new Error('Method not implemented'); } + dispose(): void { } + }; + } + + const vscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(join(fixturesFolder, 'vscode_folder')) }); + const lastActiveWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 3, openedFolderUri: undefined }); + const noVscodeFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(join(fixturesFolder, 'no_vscode_folder')) }); + const windows: ICodeWindow[] = [ + vscodeFolderWindow, + lastActiveWindow, + noVscodeFolderWindow, + ]; + + test('New window without folder when no windows exist', () => { + assert.strictEqual(findWindowOnFile([], URI.file('nonexisting'), localWorkspaceResolver), undefined); + assert.strictEqual(findWindowOnFile([], URI.file(join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), undefined); + }); + + test('Existing window with folder', () => { + assert.strictEqual(findWindowOnFile(windows, URI.file(join(fixturesFolder, 'no_vscode_folder', 'file.txt')), localWorkspaceResolver), noVscodeFolderWindow); + + assert.strictEqual(findWindowOnFile(windows, URI.file(join(fixturesFolder, 'vscode_folder', 'file.txt')), localWorkspaceResolver), vscodeFolderWindow); + + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(join(fixturesFolder, 'vscode_folder', 'nested_folder')) }); + assert.strictEqual(findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + }); + + test('More specific existing window wins', () => { + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(join(fixturesFolder, 'no_vscode_folder')) }); + const nestedFolderWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }); + assert.strictEqual(findWindowOnFile([window, nestedFolderWindow], URI.file(join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), nestedFolderWindow); + }); + + test('Workspace folder wins', () => { + const window: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedWorkspace: testWorkspace }); + assert.strictEqual(findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); + }); +}); diff --git a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts deleted file mode 100644 index f6dea5d873..0000000000 --- a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; - -import { restoreWindowsState, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage'; -import { IWindowState as IWindowUIState, WindowMode } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { URI } from 'vs/base/common/uri'; -import { IWindowsState, IWindowState } from 'vs/platform/windows/electron-main/windowsMainService'; - -function getUIState(): IWindowUIState { - return { - x: 0, - y: 10, - width: 100, - height: 200, - mode: 0 - }; -} - -function toWorkspace(uri: URI): IWorkspaceIdentifier { - return { - id: '1234', - configPath: uri - }; -} -function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { - assert.equal(u1 && u1.toString(), u2 && u2.toString(), message); -} - -function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { - if (!w1 || !w2) { - assert.equal(w1, w2, message); - return; - } - assert.equal(w1.id, w2.id, message); - assertEqualURI(w1.configPath, w2.configPath, message); -} - -function assertEqualWindowState(expected: IWindowState | undefined, actual: IWindowState | undefined, message?: string) { - if (!expected || !actual) { - assert.deepEqual(expected, actual, message); - return; - } - assert.equal(expected.backupPath, actual.backupPath, message); - assertEqualURI(expected.folderUri, actual.folderUri, message); - assert.equal(expected.remoteAuthority, actual.remoteAuthority, message); - assertEqualWorkspace(expected.workspace, actual.workspace, message); - assert.deepEqual(expected.uiState, actual.uiState, message); -} - -function assertEqualWindowsState(expected: IWindowsState, actual: IWindowsState, message?: string) { - assertEqualWindowState(expected.lastPluginDevelopmentHostWindow, actual.lastPluginDevelopmentHostWindow, message); - assertEqualWindowState(expected.lastActiveWindow, actual.lastActiveWindow, message); - assert.equal(expected.openedWindows.length, actual.openedWindows.length, message); - for (let i = 0; i < expected.openedWindows.length; i++) { - assertEqualWindowState(expected.openedWindows[i], actual.openedWindows[i], message); - } -} - -function assertRestoring(state: IWindowsState, message?: string) { - const stored = getWindowsStateStoreData(state); - const restored = restoreWindowsState(stored); - assertEqualWindowsState(state, restored, message); -} - -const testBackupPath1 = path.join(os.tmpdir(), 'windowStateTest', 'backupFolder1'); -const testBackupPath2 = path.join(os.tmpdir(), 'windowStateTest', 'backupFolder2'); - -const testWSPath = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'test.code-workspace')); -const testFolderURI = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'testFolder')); - -const testRemoteFolderURI = URI.parse('foo://bar/c/d'); - -suite('Windows State Storing', () => { - test('storing and restoring', () => { - let windowState: IWindowsState; - windowState = { - openedWindows: [] - }; - assertRestoring(windowState, 'no windows'); - windowState = { - openedWindows: [{ backupPath: testBackupPath1, uiState: getUIState() }] - }; - assertRestoring(windowState, 'empty workspace'); - - windowState = { - openedWindows: [{ backupPath: testBackupPath1, uiState: getUIState(), workspace: toWorkspace(testWSPath) }] - }; - assertRestoring(windowState, 'workspace'); - - windowState = { - openedWindows: [{ backupPath: testBackupPath2, uiState: getUIState(), folderUri: testFolderURI }] - }; - assertRestoring(windowState, 'folder'); - - windowState = { - openedWindows: [{ backupPath: testBackupPath1, uiState: getUIState(), folderUri: testFolderURI }, { backupPath: testBackupPath1, uiState: getUIState(), folderUri: testRemoteFolderURI, remoteAuthority: 'bar' }] - }; - assertRestoring(windowState, 'multiple windows'); - - windowState = { - lastActiveWindow: { backupPath: testBackupPath2, uiState: getUIState(), folderUri: testFolderURI }, - openedWindows: [] - }; - assertRestoring(windowState, 'lastActiveWindow'); - - windowState = { - lastPluginDevelopmentHostWindow: { backupPath: testBackupPath2, uiState: getUIState(), folderUri: testFolderURI }, - openedWindows: [] - }; - assertRestoring(windowState, 'lastPluginDevelopmentHostWindow'); - }); - - test('open 1_31', () => { - const v1_31_workspace = `{ - "openedWindows": [], - "lastActiveWindow": { - "workspace": { - "id": "a41787288b5e9cc1a61ba2dd84cd0d80", - "configPath": "/home/user/workspaces/code-and-docs.code-workspace" - }, - "backupPath": "/home/user/.config/Code - Insiders/Backups/a41787288b5e9cc1a61ba2dd84cd0d80", - "uiState": { - "mode": 0, - "x": 0, - "y": 27, - "width": 2560, - "height": 1364 - } - } - }`; - - let windowsState = restoreWindowsState(JSON.parse(v1_31_workspace)); - let expected: IWindowsState = { - openedWindows: [], - lastActiveWindow: { - backupPath: '/home/user/.config/Code - Insiders/Backups/a41787288b5e9cc1a61ba2dd84cd0d80', - uiState: { mode: WindowMode.Maximized, x: 0, y: 27, width: 2560, height: 1364 }, - workspace: { id: 'a41787288b5e9cc1a61ba2dd84cd0d80', configPath: URI.file('/home/user/workspaces/code-and-docs.code-workspace') } - } - }; - - assertEqualWindowsState(expected, windowsState, 'v1_31_workspace'); - - const v1_31_folder = `{ - "openedWindows": [], - "lastPluginDevelopmentHostWindow": { - "folderUri": { - "$mid": 1, - "fsPath": "/home/user/workspaces/testing/customdata", - "external": "file:///home/user/workspaces/testing/customdata", - "path": "/home/user/workspaces/testing/customdata", - "scheme": "file" - }, - "uiState": { - "mode": 1, - "x": 593, - "y": 617, - "width": 1625, - "height": 595 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_31_folder)); - expected = { - openedWindows: [], - lastPluginDevelopmentHostWindow: { - uiState: { mode: WindowMode.Normal, x: 593, y: 617, width: 1625, height: 595 }, - folderUri: URI.parse('file:///home/user/workspaces/testing/customdata') - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_31_folder'); - - const v1_31_empty_window = ` { - "openedWindows": [ - ], - "lastActiveWindow": { - "backupPath": "C:\\\\Users\\\\Mike\\\\AppData\\\\Roaming\\\\Code\\\\Backups\\\\1549538599815", - "uiState": { - "mode": 0, - "x": -8, - "y": -8, - "width": 2576, - "height": 1344 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_31_empty_window)); - expected = { - openedWindows: [], - lastActiveWindow: { - backupPath: 'C:\\Users\\Mike\\AppData\\Roaming\\Code\\Backups\\1549538599815', - uiState: { mode: WindowMode.Maximized, x: -8, y: -8, width: 2576, height: 1344 } - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_31_empty_window'); - - }); - - test('open 1_32', () => { - const v1_32_workspace = `{ - "openedWindows": [], - "lastActiveWindow": { - "workspaceIdentifier": { - "id": "53b714b46ef1a2d4346568b4f591028c", - "configURIPath": "file:///home/user/workspaces/testing/custom.code-workspace" - }, - "backupPath": "/home/user/.config/code-oss-dev/Backups/53b714b46ef1a2d4346568b4f591028c", - "uiState": { - "mode": 0, - "x": 0, - "y": 27, - "width": 2560, - "height": 1364 - } - } - }`; - - let windowsState = restoreWindowsState(JSON.parse(v1_32_workspace)); - let expected: IWindowsState = { - openedWindows: [], - lastActiveWindow: { - backupPath: '/home/user/.config/code-oss-dev/Backups/53b714b46ef1a2d4346568b4f591028c', - uiState: { mode: WindowMode.Maximized, x: 0, y: 27, width: 2560, height: 1364 }, - workspace: { id: '53b714b46ef1a2d4346568b4f591028c', configPath: URI.parse('file:///home/user/workspaces/testing/custom.code-workspace') } - } - }; - - assertEqualWindowsState(expected, windowsState, 'v1_32_workspace'); - - const v1_32_folder = `{ - "openedWindows": [], - "lastActiveWindow": { - "folder": "file:///home/user/workspaces/testing/folding", - "backupPath": "/home/user/.config/code-oss-dev/Backups/1daac1621c6c06f9e916ac8062e5a1b5", - "uiState": { - "mode": 1, - "x": 625, - "y": 263, - "width": 1718, - "height": 953 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_32_folder)); - expected = { - openedWindows: [], - lastActiveWindow: { - backupPath: '/home/user/.config/code-oss-dev/Backups/1daac1621c6c06f9e916ac8062e5a1b5', - uiState: { mode: WindowMode.Normal, x: 625, y: 263, width: 1718, height: 953 }, - folderUri: URI.parse('file:///home/user/workspaces/testing/folding') - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_32_folder'); - - const v1_32_empty_window = ` { - "openedWindows": [ - ], - "lastActiveWindow": { - "backupPath": "/home/user/.config/code-oss-dev/Backups/1549539668998", - "uiState": { - "mode": 1, - "x": 768, - "y": 336, - "width": 1024, - "height": 768 - } - } - }`; - - windowsState = restoreWindowsState(JSON.parse(v1_32_empty_window)); - expected = { - openedWindows: [], - lastActiveWindow: { - backupPath: '/home/user/.config/code-oss-dev/Backups/1549539668998', - uiState: { mode: WindowMode.Normal, x: 768, y: 336, width: 1024, height: 768 } - } - }; - assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); - - }); - -}); diff --git a/src/vs/platform/windows/test/node/window.test.ts b/src/vs/platform/windows/test/node/window.test.ts deleted file mode 100644 index 8942600108..0000000000 --- a/src/vs/platform/windows/test/node/window.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile, OpenContext } from 'vs/platform/windows/node/window'; -import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; -import { URI } from 'vs/base/common/uri'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; - -const fixturesFolder = getPathFromAmdModule(require, './fixtures'); - -const testWorkspace: IWorkspaceIdentifier = { - id: Date.now().toString(), - configPath: URI.file(path.join(fixturesFolder, 'workspaces.json')) -}; - -const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath); - -function options(custom?: Partial>): IBestWindowOrFolderOptions { - return { - windows: [], - newWindow: false, - context: OpenContext.CLI, - codeSettingsFolder: '_vscode', - localWorkspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; }, - ...custom - }; -} - -const vscodeFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) }; -const lastActiveWindow: IWindowContext = { lastFocusTime: 3, openedFolderUri: undefined }; -const noVscodeFolderWindow: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; -const windows: IWindowContext[] = [ - vscodeFolderWindow, - lastActiveWindow, - noVscodeFolderWindow, -]; - -suite('WindowsFinder', () => { - - test('New window without folder when no windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options()), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - newWindow: true - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - context: OpenContext.API - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), null); - assert.equal(findBestWindowOrFolderForFile(options({ - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt')) - })), null); - }); - - test('New window without folder when windows exist', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - newWindow: true - })), null); - }); - - test('Last active window', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt')) - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [lastActiveWindow, noVscodeFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')), - })), lastActiveWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')), - context: OpenContext.API - })), lastActiveWindow); - }); - - test('Existing window with folder', () => { - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')) - })), noVscodeFolderWindow); - assert.equal(findBestWindowOrFolderForFile(options({ - windows, - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')) - })), vscodeFolderWindow); - const window: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) - })), window); - }); - - test('More specific existing window wins', () => { - const window: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) }; - const nestedFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window, nestedFolderWindow], - fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt')) - })), nestedFolderWindow); - }); - - test('Workspace folder wins', () => { - const window: IWindowContext = { lastFocusTime: 1, openedWorkspace: testWorkspace }; - assert.equal(findBestWindowOrFolderForFile(options({ - windows: [window], - fileUri: URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')) - })), window); - }); -}); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 48b56cd060..ef2970a06a 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; +import { joinPath, basenameOrAuthority } from 'vs/base/common/resources'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TernarySearchTree } from 'vs/base/common/map'; import { Event } from 'vs/base/common/event'; -import { IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspaceIdentifier, IStoredWorkspaceFolder, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceFolderProvider } from 'vs/base/common/labels'; export const IWorkspaceContextService = createDecorator('contextService'); export interface IWorkspaceContextService extends IWorkspaceFolderProvider { + readonly _serviceBrand: undefined; /** @@ -58,9 +59,9 @@ export interface IWorkspaceContextService extends IWorkspaceFolderProvider { getWorkspaceFolder(resource: URI): IWorkspaceFolder | null; /** - * Return `true` if the current workspace has the given identifier otherwise `false`. + * Return `true` if the current workspace has the given identifier or root URI otherwise `false`. */ - isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean; + isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean; /** * Returns if the provided resource is inside the workspace or not. @@ -80,14 +81,6 @@ export interface IWorkspaceFoldersChangeEvent { changed: IWorkspaceFolder[]; } -export namespace IWorkspace { - export function isIWorkspace(thing: unknown): thing is IWorkspace { - return !!(thing && typeof thing === 'object' - && typeof (thing as IWorkspace).id === 'string' - && Array.isArray((thing as IWorkspace).folders)); - } -} - export interface IWorkspace { /** @@ -106,6 +99,14 @@ export interface IWorkspace { readonly configuration?: URI | null; } +export function isWorkspace(thing: unknown): thing is IWorkspace { + const candidate = thing as IWorkspace | undefined; + + return !!(candidate && typeof candidate === 'object' + && typeof candidate.id === 'string' + && Array.isArray(candidate.folders)); +} + export interface IWorkspaceFolderData { /** @@ -125,15 +126,6 @@ export interface IWorkspaceFolderData { readonly index: number; } -export namespace IWorkspaceFolder { - export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { - return !!(thing && typeof thing === 'object' - && URI.isUri((thing as IWorkspaceFolder).uri) - && typeof (thing as IWorkspaceFolder).name === 'string' - && typeof (thing as IWorkspaceFolder).toResource === 'function'); - } -} - export interface IWorkspaceFolder extends IWorkspaceFolderData { /** @@ -142,6 +134,15 @@ export interface IWorkspaceFolder extends IWorkspaceFolderData { toResource: (relativePath: string) => URI; } +export function isWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { + const candidate = thing as IWorkspaceFolder; + + return !!(candidate && typeof candidate === 'object' + && URI.isUri(candidate.uri) + && typeof candidate.name === 'string' + && typeof candidate.toResource === 'function'); +} + export class Workspace implements IWorkspace { private _foldersMap: TernarySearchTree = TernarySearchTree.forUris(this._ignorePathCasing); @@ -222,7 +223,7 @@ export class WorkspaceFolder implements IWorkspaceFolder { } toResource(relativePath: string): URI { - return resources.joinPath(this.uri, relativePath); + return joinPath(this.uri, relativePath); } toJSON(): IWorkspaceFolderData { @@ -231,43 +232,5 @@ export class WorkspaceFolder implements IWorkspaceFolder { } export function toWorkspaceFolder(resource: URI): WorkspaceFolder { - return new WorkspaceFolder({ uri: resource, index: 0, name: resources.basenameOrAuthority(resource) }, { uri: resource.toString() }); -} - -export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI): WorkspaceFolder[] { - let result: WorkspaceFolder[] = []; - let seen: Set = new Set(); - - const relativeTo = resources.dirname(workspaceConfigFile); - for (let configuredFolder of configuredFolders) { - let uri: URI | null = null; - if (isRawFileWorkspaceFolder(configuredFolder)) { - if (configuredFolder.path) { - uri = resources.resolvePath(relativeTo, configuredFolder.path); - } - } else if (isRawUriWorkspaceFolder(configuredFolder)) { - try { - uri = URI.parse(configuredFolder.uri); - // this makes sure all workspace folder are absolute - if (uri.path[0] !== '/') { - uri = uri.with({ path: '/' + uri.path }); - } - } catch (e) { - console.warn(e); - // ignore - } - } - if (uri) { - // remove duplicates - let comparisonKey = resources.getComparisonKey(uri); - if (!seen.has(comparisonKey)) { - seen.add(comparisonKey); - - const name = configuredFolder.name || resources.basenameOrAuthority(uri); - result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); - } - } - } - - return result; + return new WorkspaceFolder({ uri: resource, index: 0, name: basenameOrAuthority(resource) }, { uri: resource.toString() }); } diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 9a29aded95..e8547a9df3 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -4,26 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import { Workspace, toWorkspaceFolders, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { join } from 'vs/base/common/path'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; -import { IRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import { IRawFileWorkspaceFolder, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; import { isLinux, isWindows } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; suite('Workspace', () => { const fileFolder = isWindows ? 'c:\\src' : '/src'; const abcFolder = isWindows ? 'c:\\abc' : '/abc'; - const testFolderUri = URI.file(path.join(fileFolder, 'test')); - const mainFolderUri = URI.file(path.join(fileFolder, 'main')); - const test1FolderUri = URI.file(path.join(fileFolder, 'test1')); - const test2FolderUri = URI.file(path.join(fileFolder, 'test2')); - const test3FolderUri = URI.file(path.join(fileFolder, 'test3')); - const abcTest1FolderUri = URI.file(path.join(abcFolder, 'test1')); - const abcTest3FolderUri = URI.file(path.join(abcFolder, 'test3')); + const testFolderUri = URI.file(join(fileFolder, 'test')); + const mainFolderUri = URI.file(join(fileFolder, 'main')); + const test1FolderUri = URI.file(join(fileFolder, 'test1')); + const test2FolderUri = URI.file(join(fileFolder, 'test2')); + const test3FolderUri = URI.file(join(fileFolder, 'test3')); + const abcTest1FolderUri = URI.file(join(abcFolder, 'test1')); + const abcTest3FolderUri = URI.file(join(abcFolder, 'test3')); - const workspaceConfigUri = URI.file(path.join(fileFolder, 'test.code-workspace')); + const workspaceConfigUri = URI.file(join(fileFolder, 'test.code-workspace')); test('getFolder returns the folder with given uri', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 2 }); @@ -31,187 +32,187 @@ suite('Workspace', () => { const actual = testObject.getFolder(expected.uri); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); }); test('getFolder returns the folder if the uri is sub', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 0 }); let testObject = new Workspace('', [expected, new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 1 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 2 })], null, () => !isLinux); - const actual = testObject.getFolder(URI.file(path.join(fileFolder, 'test/a'))); + const actual = testObject.getFolder(URI.file(join(fileFolder, 'test/a'))); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); }); test('getFolder returns the closest folder if the uri is sub', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 2 }); let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected], null, () => !isLinux); - const actual = testObject.getFolder(URI.file(path.join(fileFolder, 'test/a'))); + const actual = testObject.getFolder(URI.file(join(fileFolder, 'test/a'))); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); }); test('getFolder returns the folder even if the uri has query path', () => { const expected = new WorkspaceFolder({ uri: testFolderUri, name: '', index: 2 }); let testObject = new Workspace('', [new WorkspaceFolder({ uri: mainFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 }), expected], null, () => !isLinux); - const actual = testObject.getFolder(URI.file(path.join(fileFolder, 'test/a')).with({ query: 'somequery' })); + const actual = testObject.getFolder(URI.file(join(fileFolder, 'test/a')).with({ query: 'somequery' })); - assert.equal(actual, expected); + assert.strictEqual(actual, expected); }); test('getFolder returns null if the uri is not sub', () => { let testObject = new Workspace('', [new WorkspaceFolder({ uri: testFolderUri, name: '', index: 0 }), new WorkspaceFolder({ uri: URI.file('/src/code'), name: '', index: 1 })], null, () => !isLinux); - const actual = testObject.getFolder(URI.file(path.join(fileFolder, 'main/a'))); + const actual = testObject.getFolder(URI.file(join(fileFolder, 'main/a'))); - assert.equal(actual, undefined); + assert.strictEqual(actual, null); }); test('toWorkspaceFolders with single absolute folder', () => { - const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 1); - assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test'); + assert.strictEqual(actual.length, 1); + assert.strictEqual(actual[0].uri.fsPath, testFolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test'); }); test('toWorkspaceFolders with single relative folder', () => { - const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 1); - assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); - assert.equal((actual[0].raw).path, './test'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test'); + assert.strictEqual(actual.length, 1); + assert.strictEqual(actual[0].uri.fsPath, testFolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, './test'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test'); }); test('toWorkspaceFolders with single absolute folder with name', () => { - const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 1); + assert.strictEqual(actual.length, 1); - assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'hello'); + assert.strictEqual(actual[0].uri.fsPath, testFolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'hello'); }); test('toWorkspaceFolders with multiple unique absolute folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 3); - assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test2'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test2'); + assert.strictEqual(actual.length, 3); + assert.strictEqual(actual[0].uri.fsPath, test2FolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test2'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test2'); - assert.equal(actual[1].uri.fsPath, test3FolderUri.fsPath); - assert.equal((actual[1].raw).path, '/src/test3'); - assert.equal(actual[1].index, 1); - assert.equal(actual[1].name, 'test3'); + assert.strictEqual(actual[1].uri.fsPath, test3FolderUri.fsPath); + assert.strictEqual((actual[1].raw).path, '/src/test3'); + assert.strictEqual(actual[1].index, 1); + assert.strictEqual(actual[1].name, 'test3'); - assert.equal(actual[2].uri.fsPath, test1FolderUri.fsPath); - assert.equal((actual[2].raw).path, '/src/test1'); - assert.equal(actual[2].index, 2); - assert.equal(actual[2].name, 'test1'); + assert.strictEqual(actual[2].uri.fsPath, test1FolderUri.fsPath); + assert.strictEqual((actual[2].raw).path, '/src/test1'); + assert.strictEqual(actual[2].index, 2); + assert.strictEqual(actual[2].name, 'test1'); }); test('toWorkspaceFolders with multiple unique absolute folders with names', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 3); - assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test2'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test2'); + assert.strictEqual(actual.length, 3); + assert.strictEqual(actual[0].uri.fsPath, test2FolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test2'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test2'); - assert.equal(actual[1].uri.fsPath, test3FolderUri.fsPath); - assert.equal((actual[1].raw).path, '/src/test3'); - assert.equal(actual[1].index, 1); - assert.equal(actual[1].name, 'noName'); + assert.strictEqual(actual[1].uri.fsPath, test3FolderUri.fsPath); + assert.strictEqual((actual[1].raw).path, '/src/test3'); + assert.strictEqual(actual[1].index, 1); + assert.strictEqual(actual[1].name, 'noName'); - assert.equal(actual[2].uri.fsPath, test1FolderUri.fsPath); - assert.equal((actual[2].raw).path, '/src/test1'); - assert.equal(actual[2].index, 2); - assert.equal(actual[2].name, 'test1'); + assert.strictEqual(actual[2].uri.fsPath, test1FolderUri.fsPath); + assert.strictEqual((actual[2].raw).path, '/src/test1'); + assert.strictEqual(actual[2].index, 2); + assert.strictEqual(actual[2].name, 'test1'); }); test('toWorkspaceFolders with multiple unique absolute and relative folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 3); - assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test2'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test2'); + assert.strictEqual(actual.length, 3); + assert.strictEqual(actual[0].uri.fsPath, test2FolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test2'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test2'); - assert.equal(actual[1].uri.fsPath, abcTest3FolderUri.fsPath); - assert.equal((actual[1].raw).path, '/abc/test3'); - assert.equal(actual[1].index, 1); - assert.equal(actual[1].name, 'noName'); + assert.strictEqual(actual[1].uri.fsPath, abcTest3FolderUri.fsPath); + assert.strictEqual((actual[1].raw).path, '/abc/test3'); + assert.strictEqual(actual[1].index, 1); + assert.strictEqual(actual[1].name, 'noName'); - assert.equal(actual[2].uri.fsPath, test1FolderUri.fsPath); - assert.equal((actual[2].raw).path, './test1'); - assert.equal(actual[2].index, 2); - assert.equal(actual[2].name, 'test1'); + assert.strictEqual(actual[2].uri.fsPath, test1FolderUri.fsPath); + assert.strictEqual((actual[2].raw).path, './test1'); + assert.strictEqual(actual[2].index, 2); + assert.strictEqual(actual[2].name, 'test1'); }); test('toWorkspaceFolders with multiple absolute folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 2); - assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test2'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test2'); + assert.strictEqual(actual.length, 2); + assert.strictEqual(actual[0].uri.fsPath, test2FolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test2'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test2'); - assert.equal(actual[1].uri.fsPath, test1FolderUri.fsPath); - assert.equal((actual[1].raw).path, '/src/test1'); - assert.equal(actual[1].index, 1); - assert.equal(actual[1].name, 'test1'); + assert.strictEqual(actual[1].uri.fsPath, test1FolderUri.fsPath); + assert.strictEqual((actual[1].raw).path, '/src/test1'); + assert.strictEqual(actual[1].index, 1); + assert.strictEqual(actual[1].name, 'test1'); }); test('toWorkspaceFolders with multiple absolute and relative folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 3); - assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test2'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test2'); + assert.strictEqual(actual.length, 3); + assert.strictEqual(actual[0].uri.fsPath, test2FolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test2'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test2'); - assert.equal(actual[1].uri.fsPath, test3FolderUri.fsPath); - assert.equal((actual[1].raw).path, '/src/test3'); - assert.equal(actual[1].index, 1); - assert.equal(actual[1].name, 'noName'); + assert.strictEqual(actual[1].uri.fsPath, test3FolderUri.fsPath); + assert.strictEqual((actual[1].raw).path, '/src/test3'); + assert.strictEqual(actual[1].index, 1); + assert.strictEqual(actual[1].name, 'noName'); - assert.equal(actual[2].uri.fsPath, abcTest1FolderUri.fsPath); - assert.equal((actual[2].raw).path, '/abc/test1'); - assert.equal(actual[2].index, 2); - assert.equal(actual[2].name, 'test1'); + assert.strictEqual(actual[2].uri.fsPath, abcTest1FolderUri.fsPath); + assert.strictEqual((actual[2].raw).path, '/abc/test1'); + assert.strictEqual(actual[2].index, 2); + assert.strictEqual(actual[2].name, 'test1'); }); test('toWorkspaceFolders with multiple absolute and relative folders with invalid paths', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); - assert.equal(actual.length, 3); - assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); - assert.equal((actual[0].raw).path, '/src/test2'); - assert.equal(actual[0].index, 0); - assert.equal(actual[0].name, 'test2'); + assert.strictEqual(actual.length, 3); + assert.strictEqual(actual[0].uri.fsPath, test2FolderUri.fsPath); + assert.strictEqual((actual[0].raw).path, '/src/test2'); + assert.strictEqual(actual[0].index, 0); + assert.strictEqual(actual[0].name, 'test2'); - assert.equal(actual[1].uri.fsPath, test3FolderUri.fsPath); - assert.equal((actual[1].raw).path, './test3'); - assert.equal(actual[1].index, 1); - assert.equal(actual[1].name, 'test3'); + assert.strictEqual(actual[1].uri.fsPath, test3FolderUri.fsPath); + assert.strictEqual((actual[1].raw).path, './test3'); + assert.strictEqual(actual[1].index, 1); + assert.strictEqual(actual[1].name, 'test3'); - assert.equal(actual[2].uri.fsPath, abcTest1FolderUri.fsPath); - assert.equal((actual[2].raw).path, '/abc/test1'); - assert.equal(actual[2].index, 2); - assert.equal(actual[2].name, 'test1'); + assert.strictEqual(actual[2].uri.fsPath, abcTest1FolderUri.fsPath); + assert.strictEqual((actual[2].raw).path, '/abc/test1'); + assert.strictEqual(actual[2].index, 2); + assert.strictEqual(actual[2].name, 'test1'); }); }); diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 597d4ee4c6..d6859a161f 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -5,11 +5,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; -import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { extname, isAbsolute } from 'vs/base/common/path'; -import { dirname, resolvePath, isEqualAuthority, relativePath, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; @@ -22,9 +22,16 @@ import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const WORKSPACE_EXTENSION = 'code-workspace'; +const WORKSPACE_SUFFIX = `.${WORKSPACE_EXTENSION}`; export const WORKSPACE_FILTER = [{ name: localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }]; export const UNTITLED_WORKSPACE_NAME = 'workspace.json'; +export function hasWorkspaceFileExtension(path: string | URI) { + const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); + + return ext === WORKSPACE_SUFFIX; +} + export const IWorkspacesService = createDecorator('workspacesService'); export interface IWorkspacesService { @@ -48,6 +55,8 @@ export interface IWorkspacesService { getDirtyWorkspaces(): Promise>; } +//#region Workspaces Recently Opened + export interface IRecentlyOpened { workspaces: Array; files: IRecentFile[]; @@ -61,7 +70,7 @@ export interface IRecentWorkspace { } export interface IRecentFolder { - folderUri: ISingleFolderWorkspaceIdentifier; + folderUri: URI; label?: string; } @@ -82,36 +91,114 @@ export function isRecentFile(curr: IRecent): curr is IRecentFile { return curr.hasOwnProperty('fileUri'); } -/** - * A single folder workspace identifier is just the path to the folder. - */ -export type ISingleFolderWorkspaceIdentifier = URI; +//#endregion -export interface IWorkspaceIdentifier { +//#region Identifiers / Payload + +export interface IBaseWorkspaceIdentifier { + + /** + * Every workspace (multi-root, single folder or empty) + * has a unique identifier. It is not possible to open + * a workspace with the same `id` in multiple windows + */ id: string; +} + +/** + * A single folder workspace identifier is a path to a folder + id. + */ +export interface ISingleFolderWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + + /** + * Folder path as `URI`. + */ + uri: URI; +} + +export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { + const singleFolderIdentifier = obj as ISingleFolderWorkspaceIdentifier | undefined; + + return typeof singleFolderIdentifier?.id === 'string' && URI.isUri(singleFolderIdentifier.uri); +} + +/** + * A multi-root workspace identifier is a path to a workspace file + id. + */ +export interface IWorkspaceIdentifier extends IBaseWorkspaceIdentifier { + + /** + * Workspace config file path as `URI`. + */ configPath: URI; } -export function reviveWorkspaceIdentifier(workspace: { id: string, configPath: UriComponents; }): IWorkspaceIdentifier { - return { id: workspace.id, configPath: URI.revive(workspace.configPath) }; +export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { + + // Multi root + if (workspace.configuration) { + return { + id: workspace.id, + configPath: workspace.configuration + }; + } + + // Single folder + if (workspace.folders.length === 1) { + return { + id: workspace.id, + uri: workspace.folders[0].uri + }; + } + + // Empty workspace + return undefined; } -export function isStoredWorkspaceFolder(thing: unknown): thing is IStoredWorkspaceFolder { - return isRawFileWorkspaceFolder(thing) || isRawUriWorkspaceFolder(thing); +export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { + const workspaceIdentifier = obj as IWorkspaceIdentifier | undefined; + + return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.configPath); } -export function isRawFileWorkspaceFolder(thing: any): thing is IRawFileWorkspaceFolder { - return thing - && typeof thing === 'object' - && typeof thing.path === 'string' - && (!thing.name || typeof thing.name === 'string'); +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) }; + } + + if (identifier?.configPath) { + return { id: identifier.id, configPath: URI.revive(identifier.configPath) }; + } + + return undefined; } -export function isRawUriWorkspaceFolder(thing: any): thing is IRawUriWorkspaceFolder { - return thing - && typeof thing === 'object' - && typeof thing.uri === 'string' - && (!thing.name || typeof thing.name === 'string'); +export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { + return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); +} + +export interface IEmptyWorkspaceInitializationPayload extends IBaseWorkspaceIdentifier { } + +export type IWorkspaceInitializationPayload = IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceInitializationPayload; + +//#endregion + +//#region Workspace File Utilities + +export function isStoredWorkspaceFolder(obj: unknown): obj is IStoredWorkspaceFolder { + return isRawFileWorkspaceFolder(obj) || isRawUriWorkspaceFolder(obj); +} + +export function isRawFileWorkspaceFolder(obj: unknown): obj is IRawFileWorkspaceFolder { + const candidate = obj as IRawFileWorkspaceFolder | undefined; + + return typeof candidate?.path === 'string' && (!candidate.name || typeof candidate.name === 'string'); +} + +export function isRawUriWorkspaceFolder(obj: unknown): obj is IRawUriWorkspaceFolder { + const candidate = obj as IRawUriWorkspaceFolder | undefined; + + return typeof candidate?.uri === 'string' && (!candidate.name || typeof candidate.name === 'string'); } export interface IRawFileWorkspaceFolder { @@ -151,56 +238,6 @@ export interface IEnterWorkspaceResult { backupPath?: string; } -export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { - return obj instanceof URI; -} - -export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { - const workspaceIdentifier = obj as IWorkspaceIdentifier; - - return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && workspaceIdentifier.configPath instanceof URI; -} - -export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { - if (workspace.configuration) { - return { - configPath: workspace.configuration, - id: workspace.id - }; - } - - if (workspace.folders.length === 1) { - return workspace.folders[0].uri; - } - - // Empty workspace - return undefined; -} - -export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean { - return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); -} - -export type IMultiFolderWorkspaceInitializationPayload = IWorkspaceIdentifier; -export interface ISingleFolderWorkspaceInitializationPayload { id: string; folder: ISingleFolderWorkspaceIdentifier; } -export interface IEmptyWorkspaceInitializationPayload { id: string; } - -export type IWorkspaceInitializationPayload = IMultiFolderWorkspaceInitializationPayload | ISingleFolderWorkspaceInitializationPayload | IEmptyWorkspaceInitializationPayload; - -export function isSingleFolderWorkspaceInitializationPayload(obj: any): obj is ISingleFolderWorkspaceInitializationPayload { - return isSingleFolderWorkspaceIdentifier((obj.folder as ISingleFolderWorkspaceIdentifier)); -} - -const WORKSPACE_SUFFIX = '.' + WORKSPACE_EXTENSION; - -export function hasWorkspaceFileExtension(path: string | URI) { - const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); - - return ext === WORKSPACE_SUFFIX; -} - -const SLASH = '/'; - /** * Given a folder URI and the workspace config folder, computes the IStoredWorkspaceFolder using * a relative or absolute path or a uri. @@ -212,12 +249,12 @@ const SLASH = '/'; * @param targetConfigFolderURI the folder where the workspace is living in * @param useSlashForPath if set, use forward slashes for file paths on windows */ -export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder { +export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows, extUri: IExtUri): IStoredWorkspaceFolder { if (folderURI.scheme !== targetConfigFolderURI.scheme) { return { name: folderName, uri: folderURI.toString(true) }; } - let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined; + let folderPath = !forceAbsolute ? extUri.relativePath(targetConfigFolderURI, folderURI) : undefined; if (folderPath !== undefined) { if (folderPath.length === 0) { folderPath = '.'; @@ -241,7 +278,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, } } } else { - if (!isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { + if (!extUri.isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { return { name: folderName, uri: folderURI.toString(true) }; } folderPath = folderURI.path; @@ -251,21 +288,59 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, return { name: folderName, path: folderPath }; } +export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI, extUri: IExtUri): WorkspaceFolder[] { + let result: WorkspaceFolder[] = []; + let seen: Set = new Set(); + + const relativeTo = extUri.dirname(workspaceConfigFile); + for (let configuredFolder of configuredFolders) { + let uri: URI | null = null; + if (isRawFileWorkspaceFolder(configuredFolder)) { + if (configuredFolder.path) { + uri = extUri.resolvePath(relativeTo, configuredFolder.path); + } + } else if (isRawUriWorkspaceFolder(configuredFolder)) { + try { + uri = URI.parse(configuredFolder.uri); + // this makes sure all workspace folder are absolute + if (uri.path[0] !== '/') { + uri = uri.with({ path: '/' + uri.path }); + } + } catch (e) { + console.warn(e); + // ignore + } + } + if (uri) { + // remove duplicates + let comparisonKey = extUri.getComparisonKey(uri); + if (!seen.has(comparisonKey)) { + seen.add(comparisonKey); + + const name = configuredFolder.name || extUri.basenameOrAuthority(uri); + result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); + } + } + } + + return result; +} + /** * Rewrites the content of a workspace file to be saved at a new location. * Throws an exception if file is not a valid workspace file */ -export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) { +export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI, extUri: IExtUri) { let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); - const sourceConfigFolder = dirname(configPathURI); - const targetConfigFolder = dirname(targetConfigPathURI); + const sourceConfigFolder = extUri.dirname(configPathURI); + const targetConfigFolder = extUri.dirname(targetConfigPathURI); const rewrittenFolders: IStoredWorkspaceFolder[] = []; const slashForPath = useSlashForPath(storedWorkspace.folders); for (const folder of storedWorkspace.folders) { - const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); + const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); let absolute; if (isFromUntitledWorkspace) { // if it was an untitled workspace, try to make paths relative @@ -274,7 +349,7 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, // for existing workspaces, preserve whether a path was absolute or relative absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); } - rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath)); + rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri)); } // Preserve as much of the existing workspace as possible by using jsonEdit @@ -308,12 +383,14 @@ function doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { export function useSlashForPath(storedFolders: IStoredWorkspaceFolder[]): boolean { if (isWindows) { - return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf(SLASH) >= 0); + return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf('/') >= 0); } return true; } +//#endregion + //#region Workspace Storage interface ISerializedRecentlyOpened { diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index d6e0186e0d..a369c5b1a5 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -3,18 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { IStateService } from 'vs/platform/state/node/state'; -import { app, JumpListCategory } from 'electron'; +import { app, JumpListCategory, JumpListItem } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { IWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { isEqual, dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -57,15 +57,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa declare readonly _serviceBrand: undefined; - private readonly _onRecentlyOpenedChange = new Emitter(); + private readonly _onRecentlyOpenedChange = this._register(new Emitter()); readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; - private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); + private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( @IStateService private readonly stateService: IStateService, @ILogService private readonly logService: ILogService, - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { @@ -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.workspacesMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); + this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }]))); } private handleWindowsJumpList(): void { @@ -89,40 +89,40 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } this.updateWindowsJumpList(); - this.onRecentlyOpenedChange(() => this.updateWindowsJumpList()); + this._register(this.onRecentlyOpenedChange(() => this.updateWindowsJumpList())); } - addRecentlyOpened(newlyAdded: IRecent[]): void { + addRecentlyOpened(recentToAdd: IRecent[]): void { const workspaces: Array = []; const files: IRecentFile[] = []; - for (let curr of newlyAdded) { + for (let recent of recentToAdd) { // Workspace - if (isRecentWorkspace(curr)) { - if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) { - workspaces.push(curr); + if (isRecentWorkspace(recent)) { + if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) { + workspaces.push(recent); } } // Folder - else if (isRecentFolder(curr)) { - if (indexOfFolder(workspaces, curr.folderUri) === -1) { - workspaces.push(curr); + else if (isRecentFolder(recent)) { + if (indexOfFolder(workspaces, recent.folderUri) === -1) { + workspaces.push(recent); } } // File else { - const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0; - const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0; + const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0; + const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0; if (!alreadyExistsInHistory && !shouldBeFiltered) { - files.push(curr); + files.push(recent); // Add to recent documents (Windows only, macOS later) - if (isWindows && curr.fileUri.scheme === Schemas.file) { - app.addRecentDocument(curr.fileUri.fsPath); + if (isWindows && recent.fileUri.scheme === Schemas.file) { + app.addRecentDocument(recent.fileUri.fsPath); } } } @@ -147,14 +147,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } } - removeRecentlyOpened(toRemove: URI[]): void { + removeRecentlyOpened(recentToRemove: URI[]): void { const keep = (recent: IRecent) => { const uri = location(recent); - for (const resource of toRemove) { - if (isEqual(resource, uri)) { + for (const resourceToRemove of recentToRemove) { + if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) { return false; } } + return true; }; @@ -246,13 +247,10 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Add current workspace to beginning if set const currentWorkspace = include?.config?.workspace; - if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) { + if (isWorkspaceIdentifier(currentWorkspace) && !this.workspacesManagementMainService.isUntitledWorkspace(currentWorkspace)) { workspaces.push({ workspace: currentWorkspace }); - } - - const currentFolder = include?.config?.folderUri; - if (currentFolder) { - workspaces.push({ folderUri: currentFolder }); + } else if (isSingleFolderWorkspaceIdentifier(currentWorkspace)) { + workspaces.push({ folderUri: currentWorkspace.uri }); } // Add currently files to open to the beginning if any @@ -319,8 +317,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa items: [ { type: 'task', - title: nls.localize('newWindow', "New Window"), - description: nls.localize('newWindowDesc', "Opens a new window"), + title: localize('newWindow', "New Window"), + description: localize('newWindowDesc', "Opens a new window"), program: process.execPath, args: '-n', // force new window iconPath: process.execPath, @@ -330,57 +328,59 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa }); // Recent Workspaces - try { - if (this.getRecentlyOpened().workspaces.length > 0) { + if (this.getRecentlyOpened().workspaces.length > 0) { - // The user might have meanwhile removed items from the jump list and we have to respect that - // so we need to update our list of recent paths with the choice of the user to not add them again - // Also: Windows will not show our custom category at all if there is any entry which was removed - // by the user! See https://github.com/microsoft/vscode/issues/15052 - let toRemove: URI[] = []; - for (let item of app.getJumpListSettings().removedItems) { - const args = item.args; - if (args) { - const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); - if (match) { - toRemove.push(URI.parse(match[2])); - } + // The user might have meanwhile removed items from the jump list and we have to respect that + // so we need to update our list of recent paths with the choice of the user to not add them again + // Also: Windows will not show our custom category at all if there is any entry which was removed + // by the user! See https://github.com/microsoft/vscode/issues/15052 + let toRemove: URI[] = []; + for (let item of app.getJumpListSettings().removedItems) { + const args = item.args; + if (args) { + const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); + if (match) { + toRemove.push(URI.parse(match[2])); } } - this.removeRecentlyOpened(toRemove); + } + this.removeRecentlyOpened(toRemove); - // Add entries + // Add entries + let hasWorkspaces = false; + const items: JumpListItem[] = coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { + const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; + const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); + + let description; + let args; + if (URI.isUri(workspace)) { + description = localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); + args = `--folder-uri "${workspace.toString()}"`; + } else { + hasWorkspaces = true; + description = localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); + args = `--file-uri "${workspace.configPath.toString()}"`; + } + + return { + type: 'task', + title: title.substr(0, 255), // Windows seems to be picky around the length of entries + description: description.substr(0, 255), // (see https://github.com/microsoft/vscode/issues/111177) + program: process.execPath, + args, + iconPath: 'explorer.exe', // simulate folder icon + iconIndex: 0 + }; + })); + + if (items.length > 0) { jumpList.push({ type: 'custom', - name: nls.localize('recentFolders', "Recent Workspaces"), - items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { - const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; - const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); - - let description; - let args; - if (isSingleFolderWorkspaceIdentifier(workspace)) { - description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); - args = `--folder-uri "${workspace.toString()}"`; - } else { - description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); - args = `--file-uri "${workspace.configPath.toString()}"`; - } - - return { - type: 'task', - title, - description, - program: process.execPath, - args, - iconPath: 'explorer.exe', // simulate folder icon - iconIndex: 0 - }; - })) + name: hasWorkspaces ? localize('recentFoldersAndWorkspaces', "Recent Folders & Workspaces") : localize('recentFolders', "Recent Folders"), + items }); } - } catch (error) { - this.logService.warn('updateWindowsJumpList#recentWorkspaces', error); // https://github.com/microsoft/vscode/issues/111177 } // Recent @@ -396,21 +396,24 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } private getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string { - if (isSingleFolderWorkspaceIdentifier(workspace)) { + + // Single Folder + if (URI.isUri(workspace)) { return basename(workspace); } // Workspace: Untitled if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, workspaceHome)) { - return nls.localize('untitledWorkspace', "Untitled (Workspace)"); + return localize('untitledWorkspace', "Untitled (Workspace)"); } + // Workspace: normal let filename = basename(workspace.configPath); if (filename.endsWith(WORKSPACE_EXTENSION)) { filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } - return nls.localize('workspaceName', "{0} (Workspace)", filename); + return localize('workspaceName', "{0} (Workspace)", filename); } } @@ -430,10 +433,10 @@ function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): numb return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id); } -function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number { - return arr.findIndex(folder => isRecentFolder(folder) && isEqual(folder.folderUri, candidate)); +function indexOfFolder(arr: IRecent[], candidate: URI): number { + return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); } function indexOfFile(arr: IRecentFile[], candidate: URI): number { - return arr.findIndex(file => isEqual(file.fileUri, candidate)); + return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 76603ffefd..34b6df5a4f 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -3,337 +3,79 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { join, dirname } from 'vs/base/common/path'; -import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; -import { readFileSync, existsSync, mkdirSync } from 'fs'; -import { isLinux } from 'vs/base/common/platform'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; -import { createHash } from 'crypto'; -import * as json from 'vs/base/common/json'; -import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; +import { AddFirstParameterToFunctions } from 'vs/base/common/types'; +import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -import { localize } from 'vs/nls'; -import product from 'vs/platform/product/common/product'; -import { MessageBoxOptions, BrowserWindow } from 'electron'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; -import { findWindowOnWorkspace } from 'vs/platform/windows/node/window'; -export const IWorkspacesMainService = createDecorator('workspacesMainService'); - -export interface IWorkspaceEnteredEvent { - window: ICodeWindow; - workspace: IWorkspaceIdentifier; -} - -export interface IWorkspacesMainService { - - readonly _serviceBrand: undefined; - - readonly onUntitledWorkspaceDeleted: Event; - readonly onWorkspaceEntered: Event; - - enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; - - createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; - - deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; - - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; - - resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; - getWorkspaceIdentifier(workspacePath: URI): Promise; -} - -export interface IStoredWorkspace { - folders: IStoredWorkspaceFolder[]; - remoteAuthority?: string; -} - -export class WorkspacesMainService extends Disposable implements IWorkspacesMainService { +export class WorkspacesMainService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { declare readonly _serviceBrand: undefined; - private readonly untitledWorkspacesHome: URI; // local URI that contains all untitled workspaces - - private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); - readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; - - private readonly _onWorkspaceEntered = this._register(new Emitter()); - readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; - constructor( - @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, - @ILogService private readonly logService: ILogService, - @IBackupMainService private readonly backupMainService: IBackupMainService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, + @IBackupMainService private readonly backupMainService: IBackupMainService ) { - super(); - - this.untitledWorkspacesHome = environmentService.untitledWorkspacesHome; } - resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { - if (!this.isWorkspacePath(uri)) { - return null; // does not look like a valid workspace config file - } - if (uri.scheme !== Schemas.file) { - return null; - } + //#region Workspace Management - let contents: string; - try { - contents = readFileSync(uri.fsPath, 'utf8'); - } catch (error) { - return null; // invalid workspace - } - - return this.doResolveWorkspace(uri, contents); - } - - private isWorkspacePath(uri: URI): boolean { - return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); - } - - private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { - try { - const workspace = this.doParseStoredWorkspace(path, contents); - const workspaceIdentifier = getWorkspaceIdentifier(path); - return { - id: workspaceIdentifier.id, - configPath: workspaceIdentifier.configPath, - folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath), - remoteAuthority: workspace.remoteAuthority - }; - } catch (error) { - this.logService.warn(error.toString()); + async enterWorkspace(windowId: number, path: URI): Promise { + const window = this.windowsMainService.getWindowById(windowId); + if (window) { + return this.workspacesManagementMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); } return null; } - private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { - - // Parse workspace file - let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser - - // Filter out folders which do not have a path or uri set - if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { - storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); - } else { - throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); - } - - return storedWorkspace; + createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + return this.workspacesManagementMainService.createUntitledWorkspace(folders, remoteAuthority); } - async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - await mkdirp(dirname(configPath)); - await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); - - return workspace; + deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise { + return this.workspacesManagementMainService.deleteUntitledWorkspace(workspace); } - createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { - const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); - const configPath = workspace.configPath.fsPath; - - const configPathDir = dirname(configPath); - if (!existsSync(configPathDir)) { - const configPathDirDir = dirname(configPathDir); - if (!existsSync(configPathDirDir)) { - mkdirSync(configPathDirDir); - } - mkdirSync(configPathDir); - } - - writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); - - return workspace; + getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise { + return this.workspacesManagementMainService.getWorkspaceIdentifier(workspacePath); } - private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } { - const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); - const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId); - const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); + //#endregion - const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; + //#region Workspaces History - for (const folder of folders) { - storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder)); - } + readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange; - return { - workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath), - storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority } - }; + async getRecentlyOpened(windowId: number): Promise { + return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); } - async getWorkspaceIdentifier(configPath: URI): Promise { - return getWorkspaceIdentifier(configPath); + async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { + return this.workspacesHistoryMainService.addRecentlyOpened(recents); } - isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { - return isUntitledWorkspace(workspace.configPath, this.environmentService); + async removeRecentlyOpened(windowId: number, paths: URI[]): Promise { + return this.workspacesHistoryMainService.removeRecentlyOpened(paths); } - deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { - if (!this.isUntitledWorkspace(workspace)) { - return; // only supported for untitled workspaces - } - - // Delete from disk - this.doDeleteUntitledWorkspaceSync(workspace); - - // Event - this._onUntitledWorkspaceDeleted.fire(workspace); + async clearRecentlyOpened(windowId: number): Promise { + return this.workspacesHistoryMainService.clearRecentlyOpened(); } - async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { - this.deleteUntitledWorkspaceSync(workspace); + //#endregion + + + //#region Dirty Workspaces + + async getDirtyWorkspaces(): Promise> { + return this.backupMainService.getDirtyWorkspaces(); } - private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { - const configPath = originalFSPath(workspace.configPath); - try { - - // Delete Workspace - rimrafSync(dirname(configPath)); - - // Mark Workspace Storage to be deleted - const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); - if (existsSync(workspaceStoragePath)) { - writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); - } - } catch (error) { - this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); - } - } - - getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { - let untitledWorkspaces: IUntitledWorkspaceInfo[] = []; - try { - const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); - for (const untitledWorkspacePath of untitledWorkspacePaths) { - const workspace = getWorkspaceIdentifier(untitledWorkspacePath); - const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); - if (!resolvedWorkspace) { - this.doDeleteUntitledWorkspaceSync(workspace); - } else { - untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); - } - } - } catch (error) { - if (error.code !== 'ENOENT') { - this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); - } - } - return untitledWorkspaces; - } - - async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { - if (!window || !window.win || !window.isReady) { - return null; // return early if the window is not ready or disposed - } - - const isValid = await this.isValidTargetWorkspacePath(window, windows, path); - if (!isValid) { - return null; // return early if the workspace is not valid - } - - const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); - if (!result) { - return null; - } - - // Emit as event - this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); - - return result; - } - - private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise { - if (!path) { - return true; - } - - if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, path)) { - return false; // window is already opened on a workspace with that path - } - - // Prevent overwriting a workspace that is currently opened in another window - if (findWindowOnWorkspace(windows, getWorkspaceIdentifier(path))) { - const options: MessageBoxOptions = { - title: product.nameLong, - type: 'info', - buttons: [localize('ok', "OK")], - message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)), - detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), - noLink: true - }; - - await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - - return false; - } - - return true; // OK - } - - private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { - if (!window.config) { - return null; - } - - window.focus(); - - // Register window for backups and migrate current backups over - let backupPath: string | undefined; - if (!window.config.extensionDevelopmentPath) { - backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); - } - - // if the window was opened on an untitled workspace, delete it. - if (window.openedWorkspace && this.isUntitledWorkspace(window.openedWorkspace)) { - this.deleteUntitledWorkspaceSync(window.openedWorkspace); - } - - // Update window configuration properly based on transition to workspace - window.config.folderUri = undefined; - window.config.workspace = workspace; - window.config.backupPath = backupPath; - - return { workspace, backupPath }; - } -} - -function getWorkspaceId(configPath: URI): string { - let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); - if (!isLinux) { - workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system - } - - return createHash('md5').update(workspaceConfigPath).digest('hex'); -} - -export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier { - return { - configPath, - id: getWorkspaceId(configPath) - }; + //#endregion } diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts new file mode 100644 index 0000000000..32329c2a64 --- /dev/null +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -0,0 +1,394 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; +import { join, dirname } from 'vs/base/common/path'; +import { writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; +import { promises, readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createHash } from 'crypto'; +import { parse } from 'vs/base/common/json'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { localize } from 'vs/nls'; +import product from 'vs/platform/product/common/product'; +import { MessageBoxOptions, BrowserWindow } from 'electron'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { findWindowOnWorkspaceOrFolder } from 'vs/platform/windows/electron-main/windowsFinder'; + +export const IWorkspacesManagementMainService = createDecorator('workspacesManagementMainService'); + +export interface IWorkspaceEnteredEvent { + window: ICodeWindow; + workspace: IWorkspaceIdentifier; +} + +export interface IWorkspacesManagementMainService { + + readonly _serviceBrand: undefined; + + readonly onUntitledWorkspaceDeleted: Event; + readonly onWorkspaceEntered: Event; + + enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise; + + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise; + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier; + + deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise; + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void; + + getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[]; + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean; + + resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null; + getWorkspaceIdentifier(workspacePath: URI): Promise; +} + +export interface IStoredWorkspace { + folders: IStoredWorkspaceFolder[]; + remoteAuthority?: string; +} + +export class WorkspacesManagementMainService extends Disposable implements IWorkspacesManagementMainService { + + declare readonly _serviceBrand: undefined; + + private readonly untitledWorkspacesHome = this.environmentService.untitledWorkspacesHome; // local URI that contains all untitled workspaces + + private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); + readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; + + private readonly _onWorkspaceEntered = this._register(new Emitter()); + readonly onWorkspaceEntered: Event = this._onWorkspaceEntered.event; + + constructor( + @IEnvironmentMainService private readonly environmentService: IEnvironmentMainService, + @ILogService private readonly logService: ILogService, + @IBackupMainService private readonly backupMainService: IBackupMainService, + @IDialogMainService private readonly dialogMainService: IDialogMainService + ) { + super(); + } + + resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { + if (!this.isWorkspacePath(uri)) { + return null; // does not look like a valid workspace config file + } + if (uri.scheme !== Schemas.file) { + return null; + } + + let contents: string; + try { + contents = readFileSync(uri.fsPath, 'utf8'); + } catch (error) { + return null; // invalid workspace + } + + return this.doResolveWorkspace(uri, contents); + } + + private isWorkspacePath(uri: URI): boolean { + return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri); + } + + private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { + try { + const workspace = this.doParseStoredWorkspace(path, contents); + const workspaceIdentifier = getWorkspaceIdentifier(path); + return { + id: workspaceIdentifier.id, + configPath: workspaceIdentifier.configPath, + folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath, extUriBiasedIgnorePathCase), + remoteAuthority: workspace.remoteAuthority + }; + } catch (error) { + this.logService.warn(error.toString()); + } + + return null; + } + + private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { + + // Parse workspace file + const storedWorkspace: IStoredWorkspace = parse(contents); // use fault tolerant parser + + // Filter out folders which do not have a path or uri set + if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { + storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); + } else { + throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); + } + + return storedWorkspace; + } + + async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); + const configPath = workspace.configPath.fsPath; + + await promises.mkdir(dirname(configPath), { recursive: true }); + await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; + } + + createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { + const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); + const configPath = workspace.configPath.fsPath; + + const configPathDir = dirname(configPath); + if (!existsSync(configPathDir)) { + const configPathDirDir = dirname(configPathDir); + if (!existsSync(configPathDirDir)) { + mkdirSync(configPathDirDir); + } + mkdirSync(configPathDir); + } + + writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; + } + + private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } { + const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString(); + const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId); + const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME); + + const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; + + for (const folder of folders) { + storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder, !isWindows, extUriBiasedIgnorePathCase)); + } + + return { + workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath), + storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority } + }; + } + + async getWorkspaceIdentifier(configPath: URI): Promise { + return getWorkspaceIdentifier(configPath); + } + + isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean { + return isUntitledWorkspace(workspace.configPath, this.environmentService); + } + + deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + if (!this.isUntitledWorkspace(workspace)) { + return; // only supported for untitled workspaces + } + + // Delete from disk + this.doDeleteUntitledWorkspaceSync(workspace); + + // Event + this._onUntitledWorkspaceDeleted.fire(workspace); + } + + async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise { + this.deleteUntitledWorkspaceSync(workspace); + } + + private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void { + const configPath = originalFSPath(workspace.configPath); + try { + + // Delete Workspace + rimrafSync(dirname(configPath)); + + // Mark Workspace Storage to be deleted + const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id); + if (existsSync(workspaceStoragePath)) { + writeFileSync(join(workspaceStoragePath, 'obsolete'), ''); + } + } catch (error) { + this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`); + } + } + + getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { + const untitledWorkspaces: IUntitledWorkspaceInfo[] = []; + try { + const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); + for (const untitledWorkspacePath of untitledWorkspacePaths) { + const workspace = getWorkspaceIdentifier(untitledWorkspacePath); + const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath); + if (!resolvedWorkspace) { + this.doDeleteUntitledWorkspaceSync(workspace); + } else { + untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority }); + } + } + } catch (error) { + if (error.code !== 'ENOENT') { + this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); + } + } + + return untitledWorkspaces; + } + + async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise { + if (!window || !window.win || !window.isReady) { + return null; // return early if the window is not ready or disposed + } + + const isValid = await this.isValidTargetWorkspacePath(window, windows, path); + if (!isValid) { + return null; // return early if the workspace is not valid + } + + const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path)); + if (!result) { + return null; + } + + // Emit as event + this._onWorkspaceEntered.fire({ window, workspace: result.workspace }); + + return result; + } + + private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], workspacePath?: URI): Promise { + if (!workspacePath) { + return true; + } + + if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, workspacePath)) { + return false; // window is already opened on a workspace with that path + } + + // Prevent overwriting a workspace that is currently opened in another window + if (findWindowOnWorkspaceOrFolder(windows, workspacePath)) { + const options: MessageBoxOptions = { + title: product.nameLong, + type: 'info', + buttons: [localize('ok', "OK")], + message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)), + detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), + noLink: true + }; + + await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow())); + + return false; + } + + return true; // OK + } + + private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null { + if (!window.config) { + return null; + } + + window.focus(); + + // Register window for backups and migrate current backups over + let backupPath: string | undefined; + if (!window.config.extensionDevelopmentPath) { + backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath); + } + + // if the window was opened on an untitled workspace, delete it. + if (isWorkspaceIdentifier(window.openedWorkspace) && this.isUntitledWorkspace(window.openedWorkspace)) { + this.deleteUntitledWorkspaceSync(window.openedWorkspace); + } + + // Update window configuration properly based on transition to workspace + window.config.workspace = workspace; + window.config.backupPath = backupPath; + + return { workspace, backupPath }; + } +} + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier { + + function getWorkspaceId(): string { + let configPathStr = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); + if (!isLinux) { + configPathStr = configPathStr.toLowerCase(); // sanitize for platform file system + } + + return createHash('md5').update(configPathStr).digest('hex'); + } + + return { + id: getWorkspaceId(), + configPath + }; +} + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +export function getSingleFolderWorkspaceIdentifier(folderUri: URI): ISingleFolderWorkspaceIdentifier | undefined; +export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat: Stats): ISingleFolderWorkspaceIdentifier; +export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat?: Stats): ISingleFolderWorkspaceIdentifier | undefined { + + function getFolderId(): string | undefined { + + // Remote: produce a hash from the entire URI + if (folderUri.scheme !== Schemas.file) { + return createHash('md5').update(folderUri.toString()).digest('hex'); + } + + // Local: produce a hash from the path and include creation time as salt + if (!folderStat) { + try { + folderStat = statSync(folderUri.fsPath); + } catch (error) { + return undefined; // folder does not exist + } + } + + let ctime: number | undefined; + if (isLinux) { + ctime = folderStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead! + } else if (isMacintosh) { + ctime = folderStat.birthtime.getTime(); // macOS: birthtime is fine to use as is + } else if (isWindows) { + if (typeof folderStat.birthtimeMs === 'number') { + ctime = Math.floor(folderStat.birthtimeMs); // Windows: fix precision issue in node.js 8.x to get 7.x results (see https://github.com/nodejs/node/issues/19897) + } else { + ctime = folderStat.birthtime.getTime(); + } + } + + // we use the ctime as extra salt to the ID so that we catch the case of a folder getting + // deleted and recreated. in that case we do not want to carry over previous state + return createHash('md5').update(folderUri.fsPath).update(ctime ? String(ctime) : '').digest('hex'); + } + + const folderId = getFolderId(); + if (typeof folderId === 'string') { + return { + id: folderId, + uri: folderUri + }; + } + + return undefined; // invalid folder +} diff --git a/src/vs/platform/workspaces/electron-main/workspacesService.ts b/src/vs/platform/workspaces/electron-main/workspacesService.ts deleted file mode 100644 index 0304a9e52d..0000000000 --- a/src/vs/platform/workspaces/electron-main/workspacesService.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces'; -import { URI } from 'vs/base/common/uri'; -import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; - -export class WorkspacesService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { - - declare readonly _serviceBrand: undefined; - - constructor( - @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService, - @IBackupMainService private readonly backupMainService: IBackupMainService - ) { - } - - //#region Workspace Management - - async enterWorkspace(windowId: number, path: URI): Promise { - const window = this.windowsMainService.getWindowById(windowId); - if (window) { - return this.workspacesMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path); - } - - return null; - } - - createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { - return this.workspacesMainService.createUntitledWorkspace(folders, remoteAuthority); - } - - deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise { - return this.workspacesMainService.deleteUntitledWorkspace(workspace); - } - - getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise { - return this.workspacesMainService.getWorkspaceIdentifier(workspacePath); - } - - //#endregion - - //#region Workspaces History - - readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange; - - async getRecentlyOpened(windowId: number): Promise { - return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId)); - } - - async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise { - return this.workspacesHistoryMainService.addRecentlyOpened(recents); - } - - async removeRecentlyOpened(windowId: number, paths: URI[]): Promise { - return this.workspacesHistoryMainService.removeRecentlyOpened(paths); - } - - async clearRecentlyOpened(windowId: number): Promise { - return this.workspacesHistoryMainService.clearRecentlyOpened(); - } - - //#endregion - - - //#region Dirty Workspaces - - async getDirtyWorkspaces(): Promise> { - return this.backupMainService.getDirtyWorkspaces(); - } - - //#endregion -} diff --git a/src/vs/platform/workspaces/test/common/workspaces.test.ts b/src/vs/platform/workspaces/test/common/workspaces.test.ts new file mode 100644 index 0000000000..9c73977967 --- /dev/null +++ b/src/vs/platform/workspaces/test/common/workspaces.test.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { hasWorkspaceFileExtension, toWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; + +suite('Workspaces', () => { + + test('hasWorkspaceFileExtension', () => { + assert.strictEqual(hasWorkspaceFileExtension('something'), false); + assert.strictEqual(hasWorkspaceFileExtension('something.code-workspace'), true); + }); + + test('toWorkspaceIdentifier', () => { + let identifier = toWorkspaceIdentifier({ id: 'id', folders: [] }); + assert.ok(!identifier); + assert.ok(!isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(!isWorkspaceIdentifier(identifier)); + + identifier = toWorkspaceIdentifier({ id: 'id', folders: [{ index: 0, name: 'test', toResource: () => URI.file('test'), uri: URI.file('test') }] }); + assert.ok(identifier); + assert.ok(isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(!isWorkspaceIdentifier(identifier)); + + identifier = toWorkspaceIdentifier({ id: 'id', configuration: URI.file('test.code-workspace'), folders: [] }); + assert.ok(identifier); + assert.ok(!isSingleFolderWorkspaceIdentifier(identifier)); + assert.ok(isWorkspaceIdentifier(identifier)); + }); +}); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index 4dcb17e0c6..39c7a09466 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -4,65 +4,66 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; import { IWorkspaceIdentifier, IRecentlyOpened, isRecentFolder, IRecentFolder, IRecentWorkspace, toStoreData, restoreRecentlyOpened } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; import { NullLogService } from 'vs/platform/log/common/log'; -function toWorkspace(uri: URI): IWorkspaceIdentifier { - return { - id: '1234', - configPath: uri - }; -} -function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { - assert.equal(u1 && u1.toString(), u2 && u2.toString(), message); -} - -function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { - if (!w1 || !w2) { - assert.equal(w1, w2, message); - return; - } - assert.equal(w1.id, w2.id, message); - assertEqualURI(w1.configPath, w2.configPath, message); -} - -function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyOpened, message?: string) { - assert.equal(actual.files.length, expected.files.length, message); - for (let i = 0; i < actual.files.length; i++) { - assertEqualURI(actual.files[i].fileUri, expected.files[i].fileUri, message); - assert.equal(actual.files[i].label, expected.files[i].label); - } - assert.equal(actual.workspaces.length, expected.workspaces.length, message); - for (let i = 0; i < actual.workspaces.length; i++) { - let expectedRecent = expected.workspaces[i]; - let actualRecent = actual.workspaces[i]; - if (isRecentFolder(actualRecent)) { - assertEqualURI(actualRecent.folderUri, (expectedRecent).folderUri, message); - } else { - assertEqualWorkspace(actualRecent.workspace, (expectedRecent).workspace, message); - } - assert.equal(actualRecent.label, expectedRecent.label); - } -} - -function assertRestoring(state: IRecentlyOpened, message?: string) { - const stored = toStoreData(state); - const restored = restoreRecentlyOpened(stored, new NullLogService()); - assertEqualRecentlyOpened(state, restored, message); -} - -const testWSPath = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'test.code-workspace')); -const testFileURI = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'testFile.txt')); -const testFolderURI = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'testFolder')); - -const testRemoteFolderURI = URI.parse('foo://bar/c/e'); -const testRemoteFileURI = URI.parse('foo://bar/c/d.txt'); -const testRemoteWSURI = URI.parse('foo://bar/c/test.code-workspace'); - suite('History Storage', () => { + + function toWorkspace(uri: URI): IWorkspaceIdentifier { + return { + id: '1234', + configPath: uri + }; + } + function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void { + assert.strictEqual(u1 && u1.toString(), u2 && u2.toString(), message); + } + + function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void { + if (!w1 || !w2) { + assert.strictEqual(w1, w2, message); + return; + } + assert.strictEqual(w1.id, w2.id, message); + assertEqualURI(w1.configPath, w2.configPath, message); + } + + function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyOpened, message?: string) { + assert.strictEqual(actual.files.length, expected.files.length, message); + for (let i = 0; i < actual.files.length; i++) { + assertEqualURI(actual.files[i].fileUri, expected.files[i].fileUri, message); + assert.strictEqual(actual.files[i].label, expected.files[i].label); + } + assert.strictEqual(actual.workspaces.length, expected.workspaces.length, message); + for (let i = 0; i < actual.workspaces.length; i++) { + let expectedRecent = expected.workspaces[i]; + let actualRecent = actual.workspaces[i]; + if (isRecentFolder(actualRecent)) { + assertEqualURI(actualRecent.folderUri, (expectedRecent).folderUri, message); + } else { + assertEqualWorkspace(actualRecent.workspace, (expectedRecent).workspace, message); + } + assert.strictEqual(actualRecent.label, expectedRecent.label); + } + } + + function assertRestoring(state: IRecentlyOpened, message?: string) { + const stored = toStoreData(state); + const restored = restoreRecentlyOpened(stored, new NullLogService()); + assertEqualRecentlyOpened(state, restored, message); + } + + const testWSPath = URI.file(join(tmpdir(), 'windowStateTest', 'test.code-workspace')); + const testFileURI = URI.file(join(tmpdir(), 'windowStateTest', 'testFile.txt')); + const testFolderURI = URI.file(join(tmpdir(), 'windowStateTest', 'testFolder')); + + const testRemoteFolderURI = URI.parse('foo://bar/c/e'); + const testRemoteFileURI = URI.parse('foo://bar/c/d.txt'); + const testRemoteWSURI = URI.parse('foo://bar/c/test.code-workspace'); + test('storing and restoring', () => { let ro: IRecentlyOpened; ro = { @@ -129,8 +130,5 @@ suite('History Storage', () => { }; assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); - }); - - }); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts similarity index 66% rename from src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts rename to src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index f01fc9a3f1..7bec91c1bb 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -10,108 +10,49 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WorkspacesManagementMainService, IStoredWorkspace, getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { NullLogService } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { dirname, joinPath } from 'vs/base/common/resources'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { dirname, extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; -export class TestDialogMainService implements IDialogMainService { - declare readonly _serviceBrand: undefined; +suite('WorkspacesManagementMainService', () => { - pickFileFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); + class TestDialogMainService implements IDialogMainService { + + declare readonly _serviceBrand: undefined; + + pickFileFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } + pickFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } + pickFile(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } + pickWorkspace(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } + showMessageBox(options: Electron.MessageBoxOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } + showSaveDialog(options: Electron.SaveDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } + showOpenDialog(options: Electron.OpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { throw new Error('Method not implemented.'); } } - pickFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); - } + class TestBackupMainService implements IBackupMainService { - pickFile(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); - } + declare readonly _serviceBrand: undefined; - pickWorkspace(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); - } - - showMessageBox(options: Electron.MessageBoxOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); - } - - showSaveDialog(options: Electron.SaveDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); - } - - showOpenDialog(options: Electron.OpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise { - throw new Error('Method not implemented.'); - } -} - -export class TestBackupMainService implements IBackupMainService { - - declare readonly _serviceBrand: undefined; - - isHotExitEnabled(): boolean { - throw new Error('Method not implemented.'); - } - - getWorkspaceBackups(): IWorkspaceBackupInfo[] { - throw new Error('Method not implemented.'); - } - - getFolderBackupPaths(): URI[] { - throw new Error('Method not implemented.'); - } - - getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] { - throw new Error('Method not implemented.'); - } - - registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string | undefined): string { - throw new Error('Method not implemented.'); - } - - registerFolderBackupSync(folderUri: URI): string { - throw new Error('Method not implemented.'); - } - - registerEmptyWindowBackupSync(backupFolder?: string | undefined, remoteAuthority?: string | undefined): string { - throw new Error('Method not implemented.'); - } - - unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void { - throw new Error('Method not implemented.'); - } - - unregisterFolderBackupSync(folderUri: URI): void { - throw new Error('Method not implemented.'); - } - - unregisterEmptyWindowBackupSync(backupFolder: string): void { - throw new Error('Method not implemented.'); - } - - async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> { - return []; - } -} - -suite('WorkspacesMainService', () => { - const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice'); - const untitledWorkspacesHomePath = path.join(parentDir, 'Workspaces'); - - class TestEnvironmentService extends EnvironmentMainService { - get untitledWorkspacesHome(): URI { - return URI.file(untitledWorkspacesHomePath); - } + isHotExitEnabled(): boolean { throw new Error('Method not implemented.'); } + getWorkspaceBackups(): IWorkspaceBackupInfo[] { throw new Error('Method not implemented.'); } + getFolderBackupPaths(): URI[] { throw new Error('Method not implemented.'); } + getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] { throw new Error('Method not implemented.'); } + registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string | undefined): string { throw new Error('Method not implemented.'); } + registerFolderBackupSync(folderUri: URI): string { throw new Error('Method not implemented.'); } + registerEmptyWindowBackupSync(backupFolder?: string | undefined, remoteAuthority?: string | undefined): string { throw new Error('Method not implemented.'); } + unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void { throw new Error('Method not implemented.'); } + unregisterFolderBackupSync(folderUri: URI): void { throw new Error('Method not implemented.'); } + unregisterEmptyWindowBackupSync(backupFolder: string): void { throw new Error('Method not implemented.'); } + async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> { return []; } } function createUntitledWorkspace(folders: string[], names?: string[]) { @@ -138,22 +79,33 @@ suite('WorkspacesMainService', () => { return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData))); } - const environmentService = new TestEnvironmentService(parseArgs(process.argv, OPTIONS)); - const logService = new NullLogService(); - - let service: WorkspacesMainService; + let testDir: string; + let untitledWorkspacesHomePath: string; + let environmentService: EnvironmentMainService; + let service: WorkspacesManagementMainService; setup(async () => { - service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService()); + testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesmanagementmainservice'); + untitledWorkspacesHomePath = path.join(testDir, 'Workspaces'); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + environmentService = new class TestEnvironmentService extends EnvironmentMainService { + constructor() { + super(parseArgs(process.argv, OPTIONS)); + } + get untitledWorkspacesHome(): URI { + return URI.file(untitledWorkspacesHomePath); + } + }; - return pfs.mkdirp(untitledWorkspacesHomePath); + service = new WorkspacesManagementMainService(environmentService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService()); + + return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { - return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE); + service.dispose(); + + return pfs.rimraf(testDir); }); function assertPathEquals(p1: string, p2: string): void { @@ -162,11 +114,11 @@ suite('WorkspacesMainService', () => { p2 = normalizeDriveLetter(p2); } - assert.equal(p1, p2); + assert.strictEqual(p1, p2); } function assertEqualURI(u1: URI, u2: URI): void { - assert.equal(u1.toString(), u2.toString()); + assert.strictEqual(u1.toString(), u2.toString()); } test('createWorkspace (folders)', async () => { @@ -176,7 +128,7 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); assert.ok(!(ws.folders[0]).name); @@ -190,11 +142,11 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); + assert.strictEqual((ws.folders[0]).name, 'currentworkingdirectory'); + assert.strictEqual((ws.folders[1]).name, 'tempdir'); }); test('createUntitledWorkspace (folders as other resource URIs)', async () => { @@ -207,12 +159,12 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace); - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.strictEqual(ws.folders.length, 2); + assert.strictEqual((ws.folders[0]).uri, folder1URI.toString(true)); + assert.strictEqual((ws.folders[1]).uri, folder2URI.toString(true)); assert.ok(!(ws.folders[0]).name); assert.ok(!(ws.folders[1]).name); - assert.equal(ws.remoteAuthority, 'server'); + assert.strictEqual(ws.remoteAuthority, 'server'); }); test('createWorkspaceSync (folders)', () => { @@ -222,7 +174,7 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); @@ -237,12 +189,12 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); + assert.strictEqual(ws.folders.length, 2); assertPathEquals((ws.folders[0]).path, process.cwd()); assertPathEquals((ws.folders[1]).path, os.tmpdir()); - assert.equal((ws.folders[0]).name, 'currentworkingdirectory'); - assert.equal((ws.folders[1]).name, 'tempdir'); + assert.strictEqual((ws.folders[0]).name, 'currentworkingdirectory'); + assert.strictEqual((ws.folders[1]).name, 'tempdir'); }); test('createUntitledWorkspaceSync (folders as other resource URIs)', () => { @@ -255,9 +207,9 @@ suite('WorkspacesMainService', () => { assert.ok(service.isUntitledWorkspace(workspace)); const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace; - assert.equal(ws.folders.length, 2); - assert.equal((ws.folders[0]).uri, folder1URI.toString(true)); - assert.equal((ws.folders[1]).uri, folder2URI.toString(true)); + assert.strictEqual(ws.folders.length, 2); + assert.strictEqual((ws.folders[0]).uri, folder1URI.toString(true)); + assert.strictEqual((ws.folders[1]).uri, folder2URI.toString(true)); assert.ok(!(ws.folders[0]).name); assert.ok(!(ws.folders[1]).name); @@ -273,7 +225,7 @@ suite('WorkspacesMainService', () => { workspace.configPath = URI.file(newPath); const resolved = service.resolveLocalWorkspaceSync(workspace.configPath); - assert.equal(2, resolved!.folders.length); + assert.strictEqual(2, resolved!.folders.length); assertEqualURI(resolved!.configPath, workspace.configPath); assert.ok(resolved!.id); fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace @@ -325,39 +277,39 @@ suite('WorkspacesMainService', () => { let origConfigPath = URI.file(firstConfigPath); let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); - let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); let ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir assertPathEquals((ws.folders[1]).path, '.'); assertPathEquals((ws.folders[2]).path, 'somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); assertPathEquals((ws.folders[1]).path, 'inside'); assertPathEquals((ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); + assert.strictEqual(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); assertPathEquals((ws.folders[1]).path, isWindows ? '..\\inside' : '../inside'); assertPathEquals((ws.folders[2]).path, isWindows ? '..\\inside\\somefolder' : '../inside/somefolder'); origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); - assert.equal(ws.folders.length, 3); - assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); - assert.equal((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); - assert.equal((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); + assert.strictEqual(ws.folders.length, 3); + assert.strictEqual((ws.folders[0]).uri, URI.file(folder1).toString(true)); + assert.strictEqual((ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true)); + assert.strictEqual((ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true)); fs.unlinkSync(firstConfigPath); }); @@ -369,8 +321,8 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = `// this is a comment\n${origContent}`; - let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); - assert.equal(0, newContent.indexOf('// this is a comment')); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); + assert.strictEqual(0, newContent.indexOf('// this is a comment')); service.deleteUntitledWorkspaceSync(workspace); }); @@ -381,26 +333,22 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); service.deleteUntitledWorkspaceSync(workspace); }); - test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { - if (!isWindows) { - return Promise.resolve(); - } - + (!isWindows ? test.skip : test)('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { const workspaceLocation = path.join(os.tmpdir(), 'wsloc'); const folder1Location = 'x:\\foo'; const folder2Location = '\\\\server\\share2\\some\\path'; - const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more'); + const folder3Location = path.join(workspaceLocation, 'inner', 'more'); const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]); const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, true, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assertPathEquals((ws.folders[0]).path, folder1Location); assertPathEquals((ws.folders[1]).path, folder2Location); @@ -422,17 +370,15 @@ suite('WorkspacesMainService', () => { }); test('getUntitledWorkspaceSync', async function () { - this.retries(3); - let untitled = service.getUntitledWorkspacesSync(); - assert.equal(untitled.length, 0); + assert.strictEqual(untitled.length, 0); const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]); assert.ok(fs.existsSync(untitledOne.configPath.fsPath)); untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); - assert.equal(untitledOne.id, untitled[0].workspace.id); + assert.strictEqual(1, untitled.length); + assert.strictEqual(untitledOne.id, untitled[0].workspace.id); const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]); assert.ok(fs.existsSync(untitledTwo.configPath.fsPath)); @@ -444,14 +390,50 @@ suite('WorkspacesMainService', () => { if (untitled.length === 1) { assert.fail(`Unexpected workspaces count of 1 (expected 2), all workspaces:\n ${fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8'))}, before getUntitledWorkspacesSync: ${beforeGettingUntitledWorkspaces}`); } - assert.equal(2, untitled.length); + assert.strictEqual(2, untitled.length); service.deleteUntitledWorkspaceSync(untitledOne); untitled = service.getUntitledWorkspacesSync(); - assert.equal(1, untitled.length); + assert.strictEqual(1, untitled.length); service.deleteUntitledWorkspaceSync(untitledTwo); untitled = service.getUntitledWorkspacesSync(); - assert.equal(0, untitled.length); + assert.strictEqual(0, untitled.length); + }); + + test('getSingleWorkspaceIdentifier', async function () { + const nonLocalUri = URI.parse('myscheme://server/work/p/f1'); + const nonLocalUriId = getSingleFolderWorkspaceIdentifier(nonLocalUri); + assert.ok(nonLocalUriId?.id); + + const localNonExistingUri = URI.file(path.join(testDir, 'f1')); + const localNonExistingUriId = getSingleFolderWorkspaceIdentifier(localNonExistingUri); + assert.ok(!localNonExistingUriId); + + fs.mkdirSync(path.join(testDir, 'f1')); + + const localExistingUri = URI.file(path.join(testDir, 'f1')); + const localExistingUriId = getSingleFolderWorkspaceIdentifier(localExistingUri); + assert.ok(localExistingUriId?.id); + }); + + test('workspace identifiers are stable', function () { + + // workspace identifier (local) + assert.strictEqual(getWorkspaceIdentifier(URI.file('/hello/test')).id, isWindows /* slash vs backslash */ ? '9f3efb614e2cd7924e4b8076e6c72233' : 'e36736311be12ff6d695feefe415b3e8'); + + // single folder identifier (local) + const fakeStat = { + ino: 1611312115129, + birthtimeMs: 1611312115129, + birthtime: new Date(1611312115129) + }; + assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.file('/hello/test'), fakeStat as fs.Stats)?.id, isWindows /* slash vs backslash */ ? '9a8441e897e5174fa388bc7ef8f7a710' : '1d726b3d516dc2a6d343abf4797eaaef'); + + // workspace identifier (remote) + assert.strictEqual(getWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test')).id, '786de4f224d57691f218dc7f31ee2ee3'); + + // single folder identifier (remote) + assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test'))?.id, '786de4f224d57691f218dc7f31ee2ee3'); }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index d121745ba8..93d90973de 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1450,6 +1450,21 @@ declare module 'vscode' { dispose(): void; } + /** + * An error type that should be used to signal cancellation of an operation. + * + * This type can be used in response to a [cancellation token](#CancellationToken) + * being cancelled or when an operation is being cancelled by the + * executor of that operation. + */ + export class CancellationError extends Error { + + /** + * Creates a new cancellation error. + */ + constructor(); + } + /** * Represents a type which can release resources, such * as event listening or a timer. @@ -1700,8 +1715,8 @@ declare module 'vscode' { /** * Options to configure the behaviour of a file open dialog. * - * * Note 1: A dialog can select files, folders, or both. This is not true for Windows - * which enforces to open either files or folder, but *not both*. + * * Note 1: On Windows and Linux, a file dialog cannot be both a file selector and a folder selector, so if you + * set both `canSelectFiles` and `canSelectFolders` to `true` on these platforms, a folder selector will be shown. * * Note 2: Explicitly setting `canSelectFiles` and `canSelectFolders` to `false` is futile * and the editor then silently adjusts the options to select files. */ @@ -3870,8 +3885,8 @@ declare module 'vscode' { * * Note that `sortText` is only used for the initial ordering of completion * items. When having a leading word (prefix) ordering is based on how - * well completion match that prefix and the initial ordering is only used - * when completions match equal. The prefix is defined by the + * well completions match that prefix and the initial ordering is only used + * when completions match equally well. The prefix is defined by the * [`range`](#CompletionItem.range)-property and can therefore be different * for each completion. */ @@ -3884,7 +3899,6 @@ declare module 'vscode' { * * Note that the filter text is matched against the leading word (prefix) which is defined * by the [`range`](#CompletionItem.range)-property. - * prefix. */ filterText?: string; @@ -4545,6 +4559,12 @@ declare module 'vscode' { * Represents a list of ranges that can be edited together along with a word pattern to describe valid range contents. */ export class LinkedEditingRanges { + /** + * Create a new linked editing ranges object. + * + * @param ranges A list of ranges that can be edited together + * @param wordPattern An optional word pattern that describes valid contents for the given ranges + */ constructor(ranges: Range[], wordPattern?: RegExp); /** @@ -4677,6 +4697,10 @@ declare module 'vscode' { * This rule will only execute if the text after the cursor matches this regular expression. */ afterText?: RegExp; + /** + * This rule will only execute if the text above the current line matches this regular expression. + */ + previousLineText?: RegExp; /** * The action to execute. */ @@ -5167,9 +5191,9 @@ declare module 'vscode' { set(uri: Uri, diagnostics: ReadonlyArray | undefined): void; /** - * Replace all entries in this collection. + * Replace diagnostics for multiple resources in this collection. * - * Diagnostics of multiple tuples of the same uri will be merged, e.g + * _Note_ that multiple tuples of the same uri will be merged, e.g * `[[file1, [d1]], [file1, [d2]]]` is equivalent to `[[file1, [d1, d2]]]`. * If a diagnostics item is `undefined` as in `[file1, undefined]` * all previous but not subsequent diagnostics are removed. @@ -5413,6 +5437,18 @@ declare module 'vscode' { */ color: string | ThemeColor | undefined; + /** + * The background color for this entry. + * + * *Note*: only `new ThemeColor('statusBarItem.errorBackground')` is + * supported for now. More background colors may be supported in the + * future. + * + * *Note*: when a background color is set, the statusbar may override + * the `color` choice to ensure the entry is readable in all themes. + */ + backgroundColor: ThemeColor | undefined; + /** * [`Command`](#Command) or identifier of a command to run on click. * @@ -5789,6 +5825,11 @@ declare module 'vscode' { setKeysForSync(keys: string[]): void; }; + /** + * A storage utility for secrets. + */ + readonly secrets: SecretStorage; + /** * The uri of the directory containing the extension. */ @@ -5926,6 +5967,48 @@ declare module 'vscode' { update(key: string, value: any): Thenable; } + /** + * The event data that is fired when a secret is added or removed. + */ + export interface SecretStorageChangeEvent { + /** + * The key of the secret that has changed. + */ + readonly key: string; + } + + /** + * Represents a storage utility for secrets, information that is + * sensitive. + */ + export interface SecretStorage { + /** + * Retrieve a secret that was stored with key. Returns undefined if there + * is no password matching that key. + * @param key The key the secret was stored under. + * @returns The stored value or `undefined`. + */ + get(key: string): Thenable; + + /** + * Store a secret under a given key. + * @param key The key to store the secret under. + * @param value The secret. + */ + store(key: string, value: string): Thenable; + + /** + * Remove a secret from storage. + * @param key The key the secret was stored under. + */ + delete(key: string): Thenable; + + /** + * Fires when a secret is stored or deleted. + */ + onDidChange: Event; + } + /** * Represents a color theme kind. */ @@ -7726,7 +7809,7 @@ declare module 'vscode' { * your extension should first check to see if any backups exist for the resource. If there is a backup, your * extension should load the file contents from there instead of from the resource in the workspace. * - * `backup` is triggered approximately one second after the the user stops editing the document. If the user + * `backup` is triggered approximately one second after the user stops editing the document. If the user * rapidly edits the document, `backup` will not be invoked until the editing stops. * * `backup` is not invoked when `auto save` is enabled (since auto save already persists the resource). @@ -8849,23 +8932,27 @@ declare module 'vscode' { getParent?(element: T): ProviderResult; /** - * Called only on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. + * Called on hover to resolve the [TreeItem](#TreeItem.tooltip) property if it is undefined. + * Called on tree item click/open to resolve the [TreeItem](#TreeItem.command) property if it is undefined. * Only properties that were undefined can be resolved in `resolveTreeItem`. * Functionality may be expanded later to include being called to resolve other missing * properties on selection and/or on open. * * Will only ever be called once per TreeItem. * + * onDidChangeTreeData should not be triggered from within resolveTreeItem. + * * *Note* that this function is called when tree items are already showing in the UI. - * Because of that, no property that changes the presentation (label, description, command, etc.) + * Because of that, no property that changes the presentation (label, description, etc.) * can be changed. * - * @param element The object associated with the TreeItem * @param item Undefined properties of `item` should be set then `item` should be returned. + * @param element The object associated with the TreeItem. + * @param token A cancellation token. * @return The resolved tree item or a thenable that resolves to such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ - resolveTreeItem?(item: TreeItem, element: T): ProviderResult; + resolveTreeItem?(item: TreeItem, element: T, token: CancellationToken): ProviderResult; } export class TreeItem { @@ -9186,7 +9273,7 @@ declare module 'vscode' { * Implement to handle when the number of rows and columns that fit into the terminal panel * changes, for example when font size changes or when the panel is resized. The initial * state of a terminal's dimensions should be treated as `undefined` until this is triggered - * as the size of a terminal isn't know until it shows up in the user interface. + * as the size of a terminal isn't known until it shows up in the user interface. * * When dimensions are overridden by * [onDidOverrideDimensions](#Pseudoterminal.onDidOverrideDimensions), `setDimensions` will @@ -11857,8 +11944,6 @@ declare module 'vscode' { export const onDidChange: Event; } - //#region Comments - /** * Collapsible state of a [comment thread](#CommentThread) */ @@ -12125,7 +12210,7 @@ declare module 'vscode' { /** * Optional reaction handler for creating and deleting reactions on a [comment](#Comment). */ - reactionHandler?: (comment: Comment, reaction: CommentReaction) => Promise; + reactionHandler?: (comment: Comment, reaction: CommentReaction) => Thenable; /** * Dispose this comment controller. @@ -12202,6 +12287,9 @@ declare module 'vscode' { * on the accounts activity bar icon. An entry for the extension will be added under the menu to sign in. This * allows quietly prompting the user to sign in. * + * If there is a matching session but the extension has not been granted access to it, setting this to true + * will also result in an immediate modal dialog, and false will add a numbered badge to the accounts icon. + * * Defaults to false. */ createIfNone?: boolean; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6bf295584a..4ba6fb0963 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,7 +16,7 @@ declare module 'vscode' { - // #region auth provider: https://github.com/microsoft/vscode/issues/88309 + //#region auth provider: https://github.com/microsoft/vscode/issues/88309 /** * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. @@ -54,28 +54,9 @@ declare module 'vscode' { } /** - * **WARNING** When writing an AuthenticationProvider, `id` should be treated as part of your extension's - * API, changing it is a breaking change for all extensions relying on the provider. The id is - * treated case-sensitively. + * A provider for performing authentication to a service. */ export interface AuthenticationProvider { - /** - * Used as an identifier for extensions trying to work with a particular - * provider: 'microsoft', 'github', etc. id must be unique, registering - * another provider with the same id will fail. - */ - readonly id: string; - - /** - * The human-readable name of the provider. - */ - readonly label: string; - - /** - * Whether it is possible to be signed into multiple accounts at once with this provider - */ - readonly supportsMultipleAccounts: boolean; - /** * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. @@ -85,31 +66,48 @@ declare module 'vscode' { /** * Returns an array of current sessions. */ + // eslint-disable-next-line vscode-dts-provider-naming getSessions(): Thenable>; /** * Prompts a user to login. */ + // eslint-disable-next-line vscode-dts-provider-naming login(scopes: string[]): Thenable; /** * Removes the session corresponding to session id. * @param sessionId The session id to log out of */ + // eslint-disable-next-line vscode-dts-provider-naming logout(sessionId: string): Thenable; } + /** + * Options for creating an [AuthenticationProvider](#AuthentcationProvider). + */ + export interface AuthenticationProviderOptions { + /** + * Whether it is possible to be signed into multiple accounts at once with this provider. + * If not specified, will default to false. + */ + readonly supportsMultipleAccounts?: boolean; + } + export namespace authentication { /** * Register an authentication provider. * * There can only be one provider per id and an error is being thrown when an id - * has already been used by another provider. + * has already been used by another provider. Ids are case-sensitive. * + * @param id The unique identifier of the provider. + * @param label The human-readable name of the provider. * @param provider The authentication provider provider. + * @params options Additional options for the provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; /** * @deprecated - getSession should now trigger extension activation. @@ -117,63 +115,32 @@ declare module 'vscode' { */ export const onDidChangeAuthenticationProviders: Event; - /** - * @deprecated - * The ids of the currently registered authentication providers. - * @returns An array of the ids of authentication providers that are currently registered. - */ - export function getProviderIds(): Thenable>; - - /** - * @deprecated - * An array of the ids of authentication providers that are currently registered. - */ - export const providerIds: ReadonlyArray; - /** * An array of the information of authentication providers that are currently registered. */ export const providers: ReadonlyArray; /** - * @deprecated * Logout of a specific session. * @param providerId The id of the provider to use * @param sessionId The session id to remove * provider */ export function logout(providerId: string, sessionId: string): Thenable; - - /** - * Retrieve a password that was stored with key. Returns undefined if there - * is no password matching that key. - * @param key The key the password was stored under. - */ - export function getPassword(key: string): Thenable; - - /** - * Store a password under a given key. - * @param key The key to store the password under - * @param value The password - */ - export function setPassword(key: string, value: string): Thenable; - - /** - * Remove a password from storage. - * @param key The key the password was stored under. - */ - export function deletePassword(key: string): Thenable; - - /** - * Fires when a password is set or deleted. - */ - export const onDidChangePassword: Event; } //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @alexdima - resolvers + export interface MessageOptions { + /** + * Do not render a native message box. + */ + useCustom?: boolean; + } + export interface RemoteAuthorityResolverContext { resolveAttempt: number; } @@ -195,18 +162,20 @@ declare module 'vscode' { // The desired local port. If this port can't be used, then another will be chosen. localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelDescription { remoteAddress: { port: number, host: string; }; //The complete local address(ex. localhost:1234) localAddress: { port: number, host: string; } | string; + public?: boolean; } export interface Tunnel extends TunnelDescription { // Implementers of Tunnel should fire onDidDispose when dispose is called. onDidDispose: Event; - dispose(): void; + dispose(): void | Thenable; } /** @@ -251,10 +220,19 @@ declare module 'vscode' { */ tunnelFactory?: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => Thenable | undefined; - /** + /**p * Provides filtering for candidate ports. */ showCandidatePort?: (host: string, port: number, detail: string) => Thenable; + + /** + * Lets the resolver declare which tunnel factory features it supports. + * UNDER DISCUSSION! MAY CHANGE SOON. + */ + tunnelFeatures?: { + elevation: boolean; + public: boolean; + }; } export namespace workspace { @@ -751,6 +729,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region debug /** @@ -771,8 +750,7 @@ declare module 'vscode' { //#endregion - - + // eslint-disable-next-line vscode-dts-region-comments //#region @joaomoreno: SCM validation /** @@ -823,6 +801,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @joaomoreno: SCM selected provider export interface SourceControl { @@ -898,6 +877,7 @@ declare module 'vscode' { //#endregion + // eslint-disable-next-line vscode-dts-region-comments //#region @jrieken -> exclusive document filters export interface DocumentFilter { @@ -906,18 +886,9 @@ declare module 'vscode' { //#endregion - //#region @alexdima - OnEnter enhancement - export interface OnEnterRule { - /** - * This rule will only execute if the text above the this line matches this regular expression. - */ - oneLineAboveText?: RegExp; - } - //#endregion - - //#region Tree View: https://github.com/microsoft/vscode/issues/61313 + //#region Tree View: https://github.com/microsoft/vscode/issues/61313 @alexr00 export interface TreeView extends Disposable { - reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; + reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number; }): Thenable; } //#endregion @@ -1001,6 +972,7 @@ declare module 'vscode' { * * @return Thenable indicating that the webview editor has been moved. */ + // eslint-disable-next-line vscode-dts-provider-naming moveCustomTextEditor?(newDocument: TextDocument, existingWebviewPanel: WebviewPanel, token: CancellationToken): Thenable; } @@ -1017,94 +989,13 @@ declare module 'vscode' { //#endregion - //#region @rebornix: Notebook + //#region https://github.com/microsoft/vscode/issues/106744, Notebooks (misc) export enum CellKind { Markdown = 1, Code = 2 } - export enum CellOutputKind { - Text = 1, - Error = 2, - Rich = 3 - } - - export interface CellStreamOutput { - outputKind: CellOutputKind.Text; - text: string; - } - - export interface CellErrorOutput { - outputKind: CellOutputKind.Error; - /** - * Exception Name - */ - ename: string; - /** - * Exception Value - */ - evalue: string; - /** - * Exception call stack - */ - traceback: string[]; - } - - export interface NotebookCellOutputMetadata { - /** - * Additional attributes of a cell metadata. - */ - custom?: { [key: string]: any }; - } - - export interface CellDisplayOutput { - outputKind: CellOutputKind.Rich; - /** - * { mime_type: value } - * - * Example: - * ```json - * { - * "outputKind": vscode.CellOutputKind.Rich, - * "data": { - * "text/html": [ - * "

Hello

" - * ], - * "text/plain": [ - * "" - * ] - * } - * } - */ - data: { [key: string]: any; }; - - readonly metadata?: NotebookCellOutputMetadata; - } - - export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; - - export class NotebookCellOutputItem { - - readonly mime: string; - readonly value: unknown; - readonly metadata?: Record; - - constructor(mime: string, value: unknown, metadata?: Record); - } - - //TODO@jrieken add id? - export class NotebookCellOutput { - - readonly outputs: NotebookCellOutputItem[]; - readonly metadata?: Record; - - constructor(outputs: NotebookCellOutputItem[], metadata?: Record); - - //TODO@jrieken HACK to workaround dependency issues... - toJSON(): any; - } - export enum NotebookCellRunState { Running = 1, Idle = 2, @@ -1179,9 +1070,10 @@ declare module 'vscode' { /** * Additional attributes of a cell metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; } + // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export interface NotebookCell { readonly index: number; readonly notebook: NotebookDocument; @@ -1189,7 +1081,9 @@ declare module 'vscode' { readonly cellKind: CellKind; readonly document: TextDocument; readonly language: string; + /** @deprecated use WorkspaceEdit.replaceCellOutput */ outputs: CellOutput[]; + /** @deprecated use WorkspaceEdit.replaceCellMetadata */ metadata: NotebookCellMetadata; } @@ -1229,7 +1123,7 @@ declare module 'vscode' { /** * Additional attributes of the document metadata. */ - custom?: { [key: string]: any }; + custom?: { [key: string]: any; }; /** * The document's current run state @@ -1241,6 +1135,11 @@ declare module 'vscode' { * When false, insecure outputs like HTML, JavaScript, SVG will not be rendered. */ trusted?: boolean; + + /** + * Languages the document supports + */ + languages?: string[]; } export interface NotebookDocumentContentOptions { @@ -1270,39 +1169,6 @@ declare module 'vscode' { metadata: NotebookDocumentMetadata; } - export interface NotebookConcatTextDocument { - uri: Uri; - isClosed: boolean; - dispose(): void; - onDidChange: Event; - version: number; - getText(): string; - getText(range: Range): string; - - offsetAt(position: Position): number; - positionAt(offset: number): Position; - validateRange(range: Range): Range; - validatePosition(position: Position): Position; - - locationAt(positionOrRange: Position | Range): Location; - positionAt(location: Location): Position; - contains(uri: Uri): boolean - } - - export interface WorkspaceEdit { - replaceNotebookMetadata(uri: Uri, value: NotebookDocumentMetadata): void; - replaceNotebookCells(uri: Uri, start: number, end: number, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; - replaceNotebookCellOutput(uri: Uri, index: number, outputs: (NotebookCellOutput | CellOutput)[], metadata?: WorkspaceEditEntryMetadata): void; - replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; - } - - export interface NotebookEditorEdit { - replaceMetadata(value: NotebookDocumentMetadata): void; - replaceCells(start: number, end: number, cells: NotebookCellData[]): void; - replaceCellOutput(index: number, outputs: (NotebookCellOutput | CellOutput)[]): void; - replaceCellMetadata(index: number, metadata: NotebookCellMetadata): void; - } - export interface NotebookCellRange { readonly start: number; /** @@ -1320,11 +1186,17 @@ declare module 'vscode' { * The range will always be revealed in the center of the viewport. */ InCenter = 1, + /** * If the range is outside the viewport, it will be revealed in the center of the viewport. * Otherwise, it will be revealed with as little scrolling as possible. */ InCenterIfOutsideViewport = 2, + + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 } export interface NotebookEditor { @@ -1336,68 +1208,44 @@ declare module 'vscode' { /** * The primary selected cell on this notebook editor. */ + // todo@API should not be undefined, rather a default readonly selection?: NotebookCell; + // @rebornix + // todo@API should replace selection + // never empty! + // primary/secondary selections + // readonly selections: NotebookCellRange[]; /** * The current visible ranges in the editor (vertically). */ readonly visibleRanges: NotebookCellRange[]; + revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; + /** * The column in which this editor shows. */ + // @jrieken + // todo@API maybe never undefined because notebooks always show in the editor area (unlike text editors) + // maybe for notebook diff editor readonly viewColumn?: ViewColumn; /** * Fired when the panel is disposed. */ + // @rebornix REMOVE/REplace NotebookCommunication + // todo@API fishy? notebooks are public objects, there should be a "global" events for this readonly onDidDispose: Event; - - /** - * Active kernel used in the editor - */ - readonly kernel?: NotebookKernel; - - /** - * Fired when the output hosting webview posts a message. - */ - readonly onDidReceiveMessage: Event; - /** - * Post a message to the output hosting webview. - * - * Messages are only delivered if the editor is live. - * - * @param message Body of the message. This must be a string or other json serializable object. - */ - postMessage(message: any): Thenable; - - /** - * Convert a uri for the local file system to one that can be used inside outputs webview. - */ - asWebviewUri(localResource: Uri): Uri; - - /** - * Perform an edit on the notebook associated with this notebook editor. - * - * The given callback-function is invoked with an [edit-builder](#NotebookEditorEdit) which must - * be used to make edits. Note that the edit-builder is only valid while the - * callback executes. - * - * @param callback A function which can create edits using an [edit-builder](#NotebookEditorEdit). - * @return A promise that resolves with a value indicating if the edits could be applied. - */ - edit(callback: (editBuilder: NotebookEditorEdit) => void): Thenable; - - setDecorations(decorationType: NotebookEditorDecorationType, range: NotebookCellRange): void; - - revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; } + // todo@API stale? export interface NotebookOutputSelector { mimeTypes?: string[]; } + // todo@API stale? export interface NotebookRenderRequest { output: CellDisplayOutput; mimeType: string; @@ -1424,6 +1272,7 @@ declare module 'vscode' { readonly changes: ReadonlyArray; } + // todo@API stale? export interface NotebookCellMoveEvent { /** @@ -1460,6 +1309,8 @@ declare module 'vscode' { export interface NotebookEditorSelectionChangeEvent { readonly notebookEditor: NotebookEditor; + // @rebornix + // todo@API show NotebookCellRange[] instead readonly selection?: NotebookCell; } @@ -1468,10 +1319,12 @@ declare module 'vscode' { readonly visibleRanges: ReadonlyArray; } + // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export interface NotebookCellData { readonly cellKind: CellKind; readonly source: string; readonly language: string; + // todo@API maybe use a separate data type? readonly outputs: CellOutput[]; readonly metadata: NotebookCellMetadata | undefined; } @@ -1482,71 +1335,6 @@ declare module 'vscode' { readonly metadata: NotebookDocumentMetadata; } - interface NotebookDocumentContentChangeEvent { - - /** - * The document that the edit is for. - */ - readonly document: NotebookDocument; - } - - interface NotebookDocumentEditEvent { - - /** - * The document that the edit is for. - */ - readonly document: NotebookDocument; - - /** - * Undo the edit operation. - * - * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your - * extension should restore the document and editor to the state they were in just before this - * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. - */ - undo(): Thenable | void; - - /** - * Redo the edit operation. - * - * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your - * extension should restore the document and editor to the state they were in just after this - * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. - */ - redo(): Thenable | void; - - /** - * Display name describing the edit. - * - * This will be shown to users in the UI for undo/redo operations. - */ - readonly label?: string; - } - - interface NotebookDocumentBackup { - /** - * Unique identifier for the backup. - * - * This id is passed back to your extension in `openNotebook` when opening a notebook editor from a backup. - */ - readonly id: string; - - /** - * Delete the current backup. - * - * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup - * is made or when the file is saved. - */ - delete(): void; - } - - interface NotebookDocumentBackupContext { - readonly destination: Uri; - } - - interface NotebookDocumentOpenContext { - readonly backupId?: string; - } /** * Communication object passed to the {@link NotebookContentProvider} and @@ -1578,24 +1366,249 @@ declare module 'vscode' { * Convert a uri for the local file system to one that can be used inside outputs webview. */ asWebviewUri(localResource: Uri): Uri; + + // @rebornix + // readonly onDidDispose: Event; + } + + // export function registerNotebookKernel(selector: string, kernel: NotebookKernel): Disposable; + + + export interface NotebookDocumentShowOptions { + viewColumn?: ViewColumn; + preserveFocus?: boolean; + preview?: boolean; + selection?: NotebookCellRange; + } + + export namespace notebook { + + export function openNotebookDocument(uri: Uri, viewType?: string): Thenable; + export const onDidOpenNotebookDocument: Event; + export const onDidCloseNotebookDocument: Event; + + // todo@API really needed? + export const onDidSaveNotebookDocument: Event; + + /** + * All currently known notebook documents. + */ + export const notebookDocuments: ReadonlyArray; + export const onDidChangeNotebookDocumentMetadata: Event; + export const onDidChangeNotebookCells: Event; + export const onDidChangeCellOutputs: Event; + export const onDidChangeCellLanguage: Event; + export const onDidChangeCellMetadata: Event; + } + + export namespace window { + export const visibleNotebookEditors: NotebookEditor[]; + export const onDidChangeVisibleNotebookEditors: Event; + export const activeNotebookEditor: NotebookEditor | undefined; + export const onDidChangeActiveNotebookEditor: Event; + export const onDidChangeNotebookEditorSelection: Event; + export const onDidChangeNotebookEditorVisibleRanges: Event; + export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookCellOutput + + export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 + } + + export interface CellStreamOutput { + outputKind: CellOutputKind.Text; + text: string; + } + + export interface CellErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename: string; + /** + * Exception Value + */ + evalue: string; + /** + * Exception call stack + */ + traceback: string[]; + } + + export interface NotebookCellOutputMetadata { + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any; }; + } + + export interface CellDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + * + * Example: + * ```json + * { + * "outputKind": vscode.CellOutputKind.Rich, + * "data": { + * "text/html": [ + * "

Hello

" + * ], + * "text/plain": [ + * "" + * ] + * } + * } + */ + data: { [key: string]: any; }; + + readonly metadata?: NotebookCellOutputMetadata; + } + + export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + + export class NotebookCellOutputItem { + + readonly mime: string; + readonly value: unknown; + readonly metadata?: Record; + + constructor(mime: string, value: unknown, metadata?: Record); + } + + // @jrieken + //TODO@API add execution count to cell output? + export class NotebookCellOutput { + + readonly id: string; + readonly outputs: NotebookCellOutputItem[]; + + constructor(outputs: NotebookCellOutputItem[]); + + //TODO@jrieken HACK to workaround dependency issues... + toJSON(): any; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorEdit + + export interface WorkspaceEdit { + replaceNotebookMetadata(uri: Uri, value: NotebookDocumentMetadata): void; + replaceNotebookCells(uri: Uri, start: number, end: number, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; + replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; + replaceNotebookCellOutput(uri: Uri, index: number, outputs: (NotebookCellOutput | CellOutput)[], metadata?: WorkspaceEditEntryMetadata): void; + appendNotebookCellOutput(uri: Uri, index: number, outputs: NotebookCellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + + // TODO@api + // https://jupyter-protocol.readthedocs.io/en/latest/messaging.html#update-display-data + // updateNotebookCellOutput(uri: Uri, index: number, outputId:string, outputs: NotebookCellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + } + + export interface NotebookEditorEdit { + replaceMetadata(value: NotebookDocumentMetadata): void; + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceCellOutput(index: number, outputs: (NotebookCellOutput | CellOutput)[]): void; + replaceCellMetadata(index: number, metadata: NotebookCellMetadata): void; + } + + export interface NotebookEditor { + /** + * Perform an edit on the notebook associated with this notebook editor. + * + * The given callback-function is invoked with an [edit-builder](#NotebookEditorEdit) which must + * be used to make edits. Note that the edit-builder is only valid while the + * callback executes. + * + * @param callback A function which can create edits using an [edit-builder](#NotebookEditorEdit). + * @return A promise that resolves with a value indicating if the edits could be applied. + */ + // @jrieken REMOVE maybe + edit(callback: (editBuilder: NotebookEditorEdit) => void): Thenable; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookContentProvider + + interface NotebookDocumentBackup { + /** + * Unique identifier for the backup. + * + * This id is passed back to your extension in `openNotebook` when opening a notebook editor from a backup. + */ + readonly id: string; + + /** + * Delete the current backup. + * + * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup + * is made or when the file is saved. + */ + delete(): void; + } + + interface NotebookDocumentBackupContext { + readonly destination: Uri; + } + + interface NotebookDocumentOpenContext { + readonly backupId?: string; } export interface NotebookContentProvider { readonly options?: NotebookDocumentContentOptions; readonly onDidChangeNotebookContentOptions?: Event; - readonly onDidChangeNotebook: Event; - /** * Content providers should always use [file system providers](#FileSystemProvider) to * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. */ - openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise; - resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise; - saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; - saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; + // eslint-disable-next-line vscode-dts-provider-naming + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + // eslint-disable-next-line vscode-dts-cancellation + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Thenable; + // eslint-disable-next-line vscode-dts-provider-naming + backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Thenable; + + // ??? + // provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult; } + export namespace notebook { + + // TODO@api use NotebookDocumentFilter instead of just notebookType:string? + // TODO@API options duplicates the more powerful variant on NotebookContentProvider + export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, + options?: NotebookDocumentContentOptions & { + /** + * Not ready for production or development use yet. + */ + viewOptions?: { + displayName: string; + filenamePattern: NotebookFilenamePattern[]; + exclusive?: boolean; + }; + } + ): Disposable; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookKernel + export interface NotebookKernel { readonly id?: string; label: string; @@ -1603,25 +1616,74 @@ declare module 'vscode' { detail?: string; isPreferred?: boolean; preloads?: Uri[]; + // @roblourens + // todo@API change to `executeCells(document: NotebookDocument, cells: NotebookCellRange[], context:{isWholeNotebooke: boolean}, token: CancelationToken): void;` + // todo@API interrupt vs cancellation, https://github.com/microsoft/vscode/issues/106741 + // interrupt?():void; executeCell(document: NotebookDocument, cell: NotebookCell): void; cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void; executeAllCells(document: NotebookDocument): void; cancelAllCellsExecution(document: NotebookDocument): void; } - export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern }; + export type NotebookFilenamePattern = GlobPattern | { include: GlobPattern; exclude: GlobPattern; }; + // todo@API why not for NotebookContentProvider? export interface NotebookDocumentFilter { viewType?: string | string[]; filenamePattern?: NotebookFilenamePattern; } + // todo@API very unclear, provider MUST not return alive object but only data object + // todo@API unclear how the flow goes export interface NotebookKernelProvider { onDidChangeKernels?: Event; provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult; resolveKernel?(kernel: T, document: NotebookDocument, webview: NotebookCommunication, token: CancellationToken): ProviderResult; } + export interface NotebookEditor { + /** + * Active kernel used in the editor + */ + // todo@API unsure about that + // kernel, kernel selection, kernel provider + readonly kernel?: NotebookKernel; + } + + export namespace notebook { + export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined; }>; + + export function registerNotebookKernelProvider(selector: NotebookDocumentFilter, provider: NotebookKernelProvider): Disposable; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookEditorDecorationType + + export interface NotebookEditor { + setDecorations(decorationType: NotebookEditorDecorationType, range: NotebookCellRange): void; + } + + export interface NotebookDecorationRenderOptions { + backgroundColor?: string | ThemeColor; + borderColor?: string | ThemeColor; + top: ThemableDecorationAttachmentRenderOptions; + } + + export interface NotebookEditorDecorationType { + readonly key: string; + dispose(): void; + } + + export namespace notebook { + export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookCellStatusBarItem + /** * Represents the alignment of status bar items. */ @@ -1651,72 +1713,7 @@ declare module 'vscode' { dispose(): void; } - export interface NotebookDecorationRenderOptions { - backgroundColor?: string | ThemeColor; - borderColor?: string | ThemeColor; - top: ThemableDecorationAttachmentRenderOptions; - } - - export interface NotebookEditorDecorationType { - readonly key: string; - dispose(): void; - } - - export interface NotebookDocumentShowOptions { - viewColumn?: ViewColumn; - preserveFocus?: boolean; - preview?: boolean; - selection?: NotebookCellRange; - } - - export namespace notebook { - export function registerNotebookContentProvider( - notebookType: string, - provider: NotebookContentProvider, - options?: NotebookDocumentContentOptions & { - /** - * Not ready for production or development use yet. - */ - viewOptions?: { - displayName: string; - filenamePattern: NotebookFilenamePattern[]; - exclusive?: boolean; - }; - } - ): Disposable; - - export function registerNotebookKernelProvider( - selector: NotebookDocumentFilter, - provider: NotebookKernelProvider - ): Disposable; - - export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType; - export function openNotebookDocument(uri: Uri, viewType?: string): Promise; - export const onDidOpenNotebookDocument: Event; - export const onDidCloseNotebookDocument: Event; - export const onDidSaveNotebookDocument: Event; - - /** - * All currently known notebook documents. - */ - export const notebookDocuments: ReadonlyArray; - export const onDidChangeNotebookDocumentMetadata: Event; - export const onDidChangeNotebookCells: Event; - export const onDidChangeCellOutputs: Event; - export const onDidChangeCellLanguage: Event; - export const onDidChangeCellMetadata: Event; - /** - * Create a document that is the concatenation of all notebook cells. By default all code-cells are included - * but a selector can be provided to narrow to down the set of cells. - * - * @param notebook - * @param selector - */ - export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; - - export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; - /** * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). * It will be disposed automatically when the notebook document is closed or the cell is deleted. @@ -1726,17 +1723,45 @@ declare module 'vscode' { * @param priority The priority of the item. Higher values mean the item should be shown more to the left. * @return A new status bar item. */ + // @roblourens + // todo@API this should be a provider, https://github.com/microsoft/vscode/issues/105809 export function createCellStatusBarItem(cell: NotebookCell, alignment?: NotebookCellStatusBarAlignment, priority?: number): NotebookCellStatusBarItem; } - export namespace window { - export const visibleNotebookEditors: NotebookEditor[]; - export const onDidChangeVisibleNotebookEditors: Event; - export const activeNotebookEditor: NotebookEditor | undefined; - export const onDidChangeActiveNotebookEditor: Event; - export const onDidChangeNotebookEditorSelection: Event; - export const onDidChangeNotebookEditorVisibleRanges: Event; - export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Promise; + //#endregion + + //#region https://github.com/microsoft/vscode/issues/106744, NotebookConcatTextDocument + + export namespace notebook { + /** + * Create a document that is the concatenation of all notebook cells. By default all code-cells are included + * but a selector can be provided to narrow to down the set of cells. + * + * @param notebook + * @param selector + */ + // @jrieken REMOVE. p_never + // todo@API really needed? we didn't find a user here + export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; + } + + export interface NotebookConcatTextDocument { + uri: Uri; + isClosed: boolean; + dispose(): void; + onDidChange: Event; + version: number; + getText(): string; + getText(range: Range): string; + + offsetAt(position: Position): number; + positionAt(offset: number): Position; + validateRange(range: Range): Range; + validatePosition(position: Position): Position; + + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; + contains(uri: Uri): boolean; } //#endregion @@ -1947,11 +1972,88 @@ declare module 'vscode' { } export namespace languages { - export function getTokenInformationAtPosition(document: TextDocument, position: Position): Promise; + export function getTokenInformationAtPosition(document: TextDocument, position: Position): Thenable; } //#endregion + //#region https://github.com/microsoft/vscode/issues/16221 + + export namespace languages { + /** + * Register a inline hints provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline hints provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerInlineHintsProvider(selector: DocumentSelector, provider: InlineHintsProvider): Disposable; + } + + /** + * Inline hint information. + */ + export class InlineHint { + /** + * The text of the hint. + */ + text: string; + /** + * The range of the hint. + */ + range: Range; + /** + * Tooltip when hover on the hint. + */ + description?: string | MarkdownString; + /** + * Whitespace before the hint. + */ + whitespaceBefore?: boolean; + /** + * Whitespace after the hint. + */ + whitespaceAfter?: boolean; + + /** + * Creates a new inline hint information object. + * + * @param text The text of the hint. + * @param range The range of the hint. + * @param hoverMessage Tooltip when hover on the hint. + * @param whitespaceBefore Whitespace before the hint. + * @param whitespaceAfter TWhitespace after the hint. + */ + constructor(text: string, range: Range, description?: string | MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean); + } + + /** + * The inline hints provider interface defines the contract between extensions and + * the inline hints feature. + */ + export interface InlineHintsProvider { + + /** + * An optional event to signal that inline hints have changed. + * @see [EventEmitter](#EventEmitter) + */ + onDidChangeInlineHints?: Event; + + /** + * @param model The document in which the command was invoked. + * @param range The range for which line hints should be computed. + * @param token A cancellation token. + * + * @return A list of arguments labels or a thenable that resolves to such. + */ + provideInlineHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + } + //#endregion + //#region https://github.com/microsoft/vscode/issues/104436 export enum ExtensionRuntime { @@ -1971,7 +2073,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/102091 export interface TextDocument { @@ -2004,7 +2105,7 @@ declare module 'vscode' { * Runs tests with the given options. If no options are given, then * all tests are run. Returns the resulting test run. */ - export function runTests(options: TestRunOptions): Thenable; + export function runTests(options: TestRunOptions, cancellationToken?: CancellationToken): Thenable; /** * Returns an observer that retrieves tests in the given workspace folder. @@ -2015,13 +2116,31 @@ declare module 'vscode' { * Returns an observer that retrieves tests in the given text document. */ export function createDocumentTestObserver(document: TextDocument): TestObserver; + + /** + * The last or selected test run. Cleared when a new test run starts. + */ + export const testResults: TestResults | undefined; + + /** + * Event that fires when the testResults are updated. + */ + export const onDidChangeTestResults: Event; + } + + export interface TestResults { + /** + * The results from the latest test run. The array contains a snapshot of + * all tests involved in the run at the moment when it completed. + */ + readonly tests: ReadonlyArray | undefined; } export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. */ - readonly tests: ReadonlyArray; + readonly tests: ReadonlyArray; /** * An event that fires when an existing test in the collection changes, or @@ -2051,23 +2170,23 @@ declare module 'vscode' { /** * List of all tests that are newly added. */ - readonly added: ReadonlyArray; + readonly added: ReadonlyArray; /** * List of existing tests that have updated. */ - readonly updated: ReadonlyArray; + readonly updated: ReadonlyArray; /** * List of existing tests that have been removed. */ - readonly removed: ReadonlyArray; + readonly removed: ReadonlyArray; /** * Highest node in the test tree under which changes were made. This can * be easily plugged into events like the TreeDataProvider update event. */ - readonly commonChangeAncestor: TestItem | null; + readonly commonChangeAncestor: RequiredTestItem | null; } /** @@ -2094,13 +2213,11 @@ declare module 'vscode' { readonly onDidChangeTest: Event; /** - * An event that should be fired when all tests that are currently defined - * have been discovered. The provider should continue to watch for changes - * and fire `onDidChangeTest` until the hierarchy is disposed. - * - * @todo can this be covered by existing progress apis? Or return a promise + * Promise that should be resolved when all tests that are initially + * defined have been discovered. The provider should continue to watch for + * changes and fire `onDidChangeTest` until the hierarchy is disposed. */ - readonly onDidDiscoverInitialTests: Event; + readonly discoveredInitialTests?: Thenable; /** * Dispose will be called when there are no longer observers interested @@ -2129,20 +2246,23 @@ declare module 'vscode' { * It's guaranteed that this method will not be called again while * there is a previous undisposed watcher for the given workspace folder. */ - createWorkspaceTestHierarchy?(workspace: WorkspaceFolder): TestHierarchy; + // eslint-disable-next-line vscode-dts-provider-naming + createWorkspaceTestHierarchy?(workspace: WorkspaceFolder): TestHierarchy | undefined; /** * Requests that tests be provided for the given document. This will * be called when tests need to be enumerated for a single open file, * for instance by code lens UI. */ - createDocumentTestHierarchy?(document: TextDocument): TestHierarchy; + // eslint-disable-next-line vscode-dts-provider-naming + createDocumentTestHierarchy?(document: TextDocument): TestHierarchy | undefined; /** * Starts a test run. This should cause {@link onDidChangeTest} to * fire with update test states during the run. * @todo this will eventually need to be able to return a summary report, coverage for example. */ + // eslint-disable-next-line vscode-dts-provider-naming runTests?(options: TestRunOptions, cancellationToken: CancellationToken): ProviderResult; } @@ -2172,6 +2292,16 @@ declare module 'vscode' { */ label: string; + /** + * Optional unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This must not change for the lifetime of a test item. + * + * If the ID is not provided, it defaults to the concatenation of the + * item's label and its parent's ID, if any. + */ + readonly id?: string; + /** * Optional description that appears next to the label. */ @@ -2208,19 +2338,30 @@ declare module 'vscode' { state: TestState; } + /** + * A {@link TestItem} with its defaults filled in. + */ + export type RequiredTestItem = { + [K in keyof Required]: K extends 'children' + ? RequiredTestItem[] + : (K extends 'description' | 'location' ? TestItem[K] : Required[K]) + }; + export enum TestRunState { // Initial state Unset = 0, + // Test will be run, but is not currently running. + Queued = 1, // Test is currently running - Running = 1, + Running = 2, // Test run has passed - Passed = 2, + Passed = 3, // Test run has failed (on an assertion) - Failed = 3, + Failed = 4, // Test run has been skipped - Skipped = 4, + Skipped = 5, // Test run failed for some other reason (compilation error, timeout, etc) - Errored = 5 + Errored = 6 } /** @@ -2294,27 +2435,180 @@ declare module 'vscode' { */ location?: Location; } + //#endregion - //#region Statusbar Item Background Color (https://github.com/microsoft/vscode/issues/110214) + //#region Opener service (https://github.com/microsoft/vscode/issues/109277) /** - * A status bar item is a status bar contribution that can - * show text and icons and run a command on click. + * Details if an `ExternalUriOpener` can open a uri. + * + * The priority is also used to rank multiple openers against each other and determine + * if an opener should be selected automatically or if the user should be prompted to + * select an opener. + * + * VS Code will try to use the best available opener, as sorted by `ExternalUriOpenerPriority`. + * If there are multiple potential "best" openers for a URI, then the user will be prompted + * to select an opener. */ - export interface StatusBarItem { + export enum ExternalUriOpenerPriority { + /** + * The opener is disabled and will never be shown to users. + * + * Note that the opener can still be used if the user specifically + * configures it in their settings. + */ + None = 0, /** - * The background color for this entry. - * - * Note: the supported colors for background are currently limited to - * `ThemeColors` with id `statusBarItem.errorBackground`. Other `ThemeColors` - * will be ignored. - * - * When setting the background color to `statusBarItem.errorBackground`, it is - * recommended to also set `color` to `statusBarItem.errorForeground`. + * The opener can open the uri but will not cause a prompt on its own + * since VS Code always contributes a built-in `Default` opener. */ - backgroundColor: ThemeColor | undefined; + Option = 1, + + /** + * The opener can open the uri. + * + * VS Code's built-in opener has `Default` priority. This means that any additional `Default` + * openers will cause the user to be prompted to select from a list of all potential openers. + */ + Default = 2, + + /** + * The opener can open the uri and should be automatically selected over any + * default openers, include the built-in one from VS Code. + * + * A preferred opener will be automatically selected if no other preferred openers + * are available. If multiple preferred openers are available, then the user + * is shown a prompt with all potential openers (not just preferred openers). + */ + Preferred = 3, + } + + /** + * Handles opening uris to external resources, such as http(s) links. + * + * Extensions can implement an `ExternalUriOpener` to open `http` links to a webserver + * inside of VS Code instead of having the link be opened by the web browser. + * + * Currently openers may only be registered for `http` and `https` uris. + */ + export interface ExternalUriOpener { + + /** + * Check if the opener can open a uri. + * + * @param uri The uri being opened. This is the uri that the user clicked on. It has + * not yet gone through port forwarding. + * @param token Cancellation token indicating that the result is no longer needed. + * + * @return Priority indicating if the opener can open the external uri. + */ + canOpenExternalUri(uri: Uri, token: CancellationToken): ExternalUriOpenerPriority | Thenable; + + /** + * Open a uri. + * + * This is invoked when: + * + * - The user clicks a link which does not have an assigned opener. In this case, first `canOpenExternalUri` + * is called and if the user selects this opener, then `openExternalUri` is called. + * - The user sets the default opener for a link in their settings and then visits a link. + * + * @param resolvedUri The uri to open. This uri may have been transformed by port forwarding, so it + * may not match the original uri passed to `canOpenExternalUri`. Use `ctx.originalUri` to check the + * original uri. + * @param ctx Additional information about the uri being opened. + * @param token Cancellation token indicating that opening has been canceled. + * + * @return Thenable indicating that the opening has completed. + */ + openExternalUri(resolvedUri: Uri, ctx: OpenExternalUriContext, token: CancellationToken): Thenable | void; + } + + /** + * Additional information about the uri being opened. + */ + interface OpenExternalUriContext { + /** + * The uri that triggered the open. + * + * This is the original uri that the user clicked on or that was passed to `openExternal.` + * Due to port forwarding, this may not match the `resolvedUri` passed to `openExternalUri`. + */ + readonly sourceUri: Uri; + } + + /** + * Additional metadata about a registered `ExternalUriOpener`. + */ + interface ExternalUriOpenerMetadata { + + /** + * List of uri schemes the opener is triggered for. + * + * Currently only `http` and `https` are supported. + */ + readonly schemes: readonly string[] + + /** + * Text displayed to the user that explains what the opener does. + * + * For example, 'Open in browser preview' + */ + readonly label: string; + } + + namespace window { + /** + * Register a new `ExternalUriOpener`. + * + * When a uri is about to be opened, an `onOpenExternalUri:SCHEME` activation event is fired. + * + * @param id Unique id of the opener, such as `myExtension.browserPreview`. This is used in settings + * and commands to identify the opener. + * @param opener Opener to register. + * @param metadata Additional information about the opener. + * + * @returns Disposable that unregisters the opener. + */ + export function registerExternalUriOpener(id: string, opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable; + } + + interface OpenExternalOptions { + /** + * Allows using openers contributed by extensions through `registerExternalUriOpener` + * when opening the resource. + * + * If `true`, VS Code will check if any contributed openers can handle the + * uri, and fallback to the default opener behavior. + * + * If it is string, this specifies the id of the `ExternalUriOpener` + * that should be used if it is available. Use `'default'` to force VS Code's + * standard external opener to be used. + */ + readonly allowContributedOpeners?: boolean | string; + } + + namespace env { + export function openExternal(target: Uri, options?: OpenExternalOptions): Thenable; + } + + //#endregion + + //#region https://github.com/Microsoft/vscode/issues/15178 + + // TODO@API must be a class + export interface OpenEditorInfo { + name: string; + resource: Uri; + } + + export namespace window { + export const openEditors: ReadonlyArray; + + // todo@API proper event type + export const onDidChangeOpenEditors: Event; } //#endregion diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 83d880ec92..2d6a6a9e09 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -11,12 +11,14 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle // --- other interested parties import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint'; import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint'; +import { IconExtensionPoint, IconFontExtensionPoint } from 'vs/workbench/services/themes/common/iconExtensionPoint'; import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint'; import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint'; // --- mainThread participants import './mainThreadBulkEdits'; import './mainThreadCodeInsets'; +import './mainThreadCLICommands'; import './mainThreadClipboard'; import './mainThreadCommands'; import './mainThreadConfiguration'; @@ -30,6 +32,7 @@ import './mainThreadDocuments'; import './mainThreadDocumentsAndEditors'; import './mainThreadEditor'; import './mainThreadEditors'; +import './mainThreadEditorTabs'; import './mainThreadErrors'; import './mainThreadExtensionService'; import './mainThreadFileSystem'; @@ -54,6 +57,7 @@ import './mainThreadTheming'; import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; +import './mainThreadUriOpeners'; import './mainThreadWindow'; import './mainThreadWebviewManager'; import './mainThreadWorkspace'; @@ -65,6 +69,7 @@ import './mainThreadTunnelService'; import './mainThreadAuthentication'; import './mainThreadTimeline'; import './mainThreadTesting'; +import './mainThreadSecretState'; import 'vs/workbench/api/common/apiCommands'; export class ExtensionPoints implements IWorkbenchContribution { @@ -75,6 +80,8 @@ export class ExtensionPoints implements IWorkbenchContribution { // Classes that handle extension points... this.instantiationService.createInstance(JSONValidationExtensionPoint); this.instantiationService.createInstance(ColorExtensionPoint); + this.instantiationService.createInstance(IconExtensionPoint); + this.instantiationService.createInstance(IconFontExtensionPoint); this.instantiationService.createInstance(TokenClassificationExtensionPoints); this.instantiationService.createInstance(LanguageConfigurationFileHandler); } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 258ac702f8..b6dc88bd67 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -7,70 +7,15 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as modes from 'vs/editor/common/modes'; import * as nls from 'vs/nls'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { IAuthenticationService, AllowedExtension, readAllowedExtensions, getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IAuthenticationService, AllowedExtension, readAllowedExtensions, getAuthenticationProviderActivationEvent, addAccountUsage, readAccountUsages, removeAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService'; import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import Severity from 'vs/base/common/severity'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { fromNow } from 'vs/base/common/date'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { isWeb } from 'vs/base/common/platform'; -import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; - -const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser', 'github.codespaces']; - -interface IAccountUsage { - extensionId: string; - extensionName: string; - lastUsed: number; -} - -function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { - const accountKey = `${providerId}-${accountName}-usages`; - const storedUsages = storageService.get(accountKey, StorageScope.GLOBAL); - let usages: IAccountUsage[] = []; - if (storedUsages) { - try { - usages = JSON.parse(storedUsages); - } catch (e) { - // ignore - } - } - - return usages; -} - -function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void { - const accountKey = `${providerId}-${accountName}-usages`; - storageService.remove(accountKey, StorageScope.GLOBAL); -} - -function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { - const accountKey = `${providerId}-${accountName}-usages`; - const usages = readAccountUsages(storageService, providerId, accountName); - - const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); - if (existingUsageIndex > -1) { - usages.splice(existingUsageIndex, 1, { - extensionId, - extensionName, - lastUsed: Date.now() - }); - } else { - usages.push({ - extensionId, - extensionName, - lastUsed: Date.now() - }); - } - - storageService.store(accountKey, JSON.stringify(usages), StorageScope.GLOBAL, StorageTarget.MACHINE); -} export class MainThreadAuthenticationProvider extends Disposable { private _accounts = new Map(); // Map account name to session ids @@ -223,12 +168,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IExtensionService private readonly extensionService: IExtensionService, - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IEncryptionService private readonly encryptionService: IEncryptionService, - @IProductService private readonly productService: IProductService + @IExtensionService private readonly extensionService: IExtensionService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -250,17 +191,6 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this._register(this.authenticationService.onDidChangeDeclaredProviders(e => { this._proxy.$setProviders(e); })); - - // {{SQL CARBON EDIT}} - avoid null reference exception - if (this.credentialsService.onDidChangePassword) { - this._register(this.credentialsService.onDidChangePassword(_ => { - this._proxy.$onDidChangePassword(); - })); - } - } - - $getProviderIds(): Promise { - return Promise.resolve(this.authenticationService.getProviderIds()); } async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): Promise { @@ -281,172 +211,17 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.authenticationService.sessionsUpdate(id, event); } - $getSessions(id: string): Promise> { - return this.authenticationService.getSessions(id); - } - - $login(providerId: string, scopes: string[]): Promise { - return this.authenticationService.login(providerId, scopes); - } - $logout(providerId: string, sessionId: string): Promise { return this.authenticationService.logout(providerId, sessionId); } - async $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { - return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); - } - - async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise { - const orderedScopes = scopes.sort().join(' '); - const sessions = (await this.$getSessions(providerId)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); - const label = this.authenticationService.getLabel(providerId); - - if (sessions.length) { - if (!this.authenticationService.supportsMultipleAccounts(providerId)) { - const session = sessions[0]; - const allowed = await this.$getSessionsPrompt(providerId, session.account.label, label, extensionId, extensionName); - if (allowed) { - return session; - } else { - throw new Error('User did not consent to login.'); - } - } - - // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this.$selectSession(providerId, label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); - return sessions.find(session => session.id === selected.id); - } else { - if (options.createIfNone) { - const isAllowed = await this.$loginPrompt(label, extensionName); - if (!isAllowed) { - throw new Error('User did not consent to login.'); - } - - const session = await this.authenticationService.login(providerId, scopes); - await this.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); - return session; - } else { - await this.$requestNewSession(providerId, scopes, extensionId, extensionName); - return undefined; - } - } - } - - async $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { - if (!potentialSessions.length) { - throw new Error('No potential sessions found'); - } - - if (clearSessionPreference) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL); - } else { - const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL); - if (existingSessionPreference) { - const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); - if (matchingSession) { - const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.label, providerName, extensionId, extensionName); - if (allowed) { - return matchingSession; - } - } - } - } - - return new Promise((resolve, reject) => { - const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: modes.AuthenticationSession }>(); - quickPick.ignoreFocusOut = true; - const items: { label: string, session?: modes.AuthenticationSession }[] = potentialSessions.map(session => { - return { - label: session.account.label, - session - }; - }); - - items.push({ - label: nls.localize('useOtherAccount', "Sign in to another account") - }); - - quickPick.items = items; - quickPick.title = nls.localize( - { - key: 'selectAccount', - comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] - }, - "The extension '{0}' wants to access a {1} account", - extensionName, - providerName); - quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); - - quickPick.onDidAccept(async _ => { - const selected = quickPick.selectedItems[0]; - - const session = selected.session ?? await this.authenticationService.login(providerId, scopes); - - const accountName = session.account.label; - - const allowList = readAllowedExtensions(this.storageService, providerId, accountName); - if (!allowList.find(allowed => allowed.id === extensionId)) { - allowList.push({ id: extensionId, name: extensionName }); - this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL, StorageTarget.USER); - } - - this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL, StorageTarget.MACHINE); - - quickPick.dispose(); - resolve(session); - }); - - quickPick.onDidHide(_ => { - if (!quickPick.selectedItems[0]) { - reject('User did not consent to account access'); - } - - quickPick.dispose(); - }); - - quickPick.show(); - }); - } - - async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { + private isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean { const allowList = readAllowedExtensions(this.storageService, providerId, accountName); const extensionData = allowList.find(extension => extension.id === extensionId); - if (extensionData) { - addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); - return true; - } - - const remoteConnection = this.remoteAgentService.getConnection(); - const isVSO = remoteConnection !== null - ? remoteConnection.remoteAuthority.startsWith('vsonline') - : isWeb; - - if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { - addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); - return true; - } - - const { choice } = await this.dialogService.show( - Severity.Info, - nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, providerName, accountName), - [nls.localize('allow', "Allow"), nls.localize('cancel', "Cancel")], - { - cancelId: 1 - } - ); - - const allow = choice === 0; - if (allow) { - addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); - allowList.push({ id: extensionId, name: extensionName }); - this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL, StorageTarget.USER); - } - - return allow; + return !!extensionData; } - async $loginPrompt(providerName: string, extensionName: string): Promise { + private async loginPrompt(providerName: string, extensionName: string): Promise { const { choice } = await this.dialogService.show( Severity.Info, nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName), @@ -459,7 +234,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return choice === 0; } - async $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise { + private async setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise { const allowList = readAllowedExtensions(this.storageService, providerId, accountName); if (!allowList.find(allowed => allowed.id === extensionId)) { allowList.push({ id: extensionId, name: extensionName }); @@ -467,48 +242,77 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL, StorageTarget.MACHINE); - addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); + } - private getFullKey(extensionId: string): string { - return `${this.productService.urlProtocol}${extensionId}`; - } + private async selectSession(providerId: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], clearSessionPreference: boolean): Promise { + if (!potentialSessions.length) { + throw new Error('No potential sessions found'); + } - async $getPassword(extensionId: string, key: string): Promise { - const fullKey = this.getFullKey(extensionId); - const password = await this.credentialsService.getPassword(fullKey, key); - const decrypted = password && await this.encryptionService.decrypt(password); - - if (decrypted) { - try { - const value = JSON.parse(decrypted); - if (value.extensionId === extensionId) { - return value.content; + if (clearSessionPreference) { + this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + } else { + const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + if (existingSessionPreference) { + const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); + if (matchingSession) { + const allowed = await this.authenticationService.showGetSessionPrompt(providerId, matchingSession.account.label, extensionId, extensionName); + if (allowed) { + return matchingSession; + } } - } catch (_) { - throw new Error('Cannot get password'); } } - return undefined; + return this.authenticationService.selectSession(providerId, extensionId, extensionName, potentialSessions); } - async $setPassword(extensionId: string, key: string, value: string): Promise { - const fullKey = this.getFullKey(extensionId); - const toEncrypt = JSON.stringify({ - extensionId, - content: value - }); - const encrypted = await this.encryptionService.encrypt(toEncrypt); - return this.credentialsService.setPassword(fullKey, key, encrypted); - } + async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise { + const orderedScopes = scopes.sort().join(' '); + const sessions = (await this.authenticationService.getSessions(providerId)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); - async $deletePassword(extensionId: string, key: string): Promise { - try { - const fullKey = this.getFullKey(extensionId); - await this.credentialsService.deletePassword(fullKey, key); - } catch (_) { - throw new Error('Cannot delete password'); + const silent = !options.createIfNone; + let session: modes.AuthenticationSession | undefined; + if (sessions.length) { + if (!this.authenticationService.supportsMultipleAccounts(providerId)) { + session = sessions[0]; + const allowed = this.isAccessAllowed(providerId, session.account.label, extensionId); + if (!allowed) { + if (!silent) { + const didAcceptPrompt = await this.authenticationService.showGetSessionPrompt(providerId, session.account.label, extensionId, extensionName); + if (!didAcceptPrompt) { + throw new Error('User did not consent to login.'); + } + } else { + this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, [session]); + } + } + } else { + if (!silent) { + session = await this.selectSession(providerId, extensionId, extensionName, sessions, !!options.clearSessionPreference); + } else { + this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, sessions); + } + } + } else { + if (!silent) { + const isAllowed = await this.loginPrompt(providerId, extensionName); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + session = await this.authenticationService.login(providerId, scopes); + await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); + } else { + await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); + } } + + if (session) { + addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + } + + return session; } } diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index 2dc8b66a7d..ccf0eb36db 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -3,29 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; -import { IExtHostContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { revive } from 'vs/base/common/marshalling'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; - -function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { - if (!data?.edits) { - return []; - } - - const result: ResourceEdit[] = []; - for (let edit of revive(data).edits) { - if (edit._type === WorkspaceEditType.File) { - result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Text) { - result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); - } else if (edit._type === WorkspaceEditType.Cell) { - result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookVersionId, edit.metadata)); - } - } - return result; -} +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IExtHostContext, IWorkspaceEditDto, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; @extHostNamedCustomer(MainContext.MainThreadBulkEdits) export class MainThreadBulkEdits implements MainThreadBulkEditsShape { diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts new file mode 100644 index 0000000000..497401594d --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import { isString } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IExtensionManifest } from 'vs/workbench/workbench.web.api'; + + +// this class contains the commands that the CLI server is reying on + +CommandsRegistry.registerCommand('_remoteCLI.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { + // TODO: discuss martin, ben where to put this + const openerService = accessor.get(IOpenerService); + openerService.open(URI.revive(uri), { openExternal: true, allowTunneling: options?.allowTunneling === true }); +}); + +interface ManageExtensionsArgs { + list?: { showVersions?: boolean, category?: string; }; + install?: (string | URI)[]; + uninstall?: string[]; + force?: boolean; +} + +CommandsRegistry.registerCommand('_remoteCLI.manageExtensions', async function (accessor: ServicesAccessor, args: ManageExtensionsArgs) { + + const instantiationService = accessor.get(IInstantiationService); + const extensionManagementServerService = accessor.get(IExtensionManagementServerService); + const remoteExtensionManagementService = extensionManagementServerService.remoteExtensionManagementServer?.extensionManagementService; + if (!remoteExtensionManagementService) { + return undefined; + } + + const cliService = instantiationService.createChild(new ServiceCollection([IExtensionManagementService, remoteExtensionManagementService])).createInstance(RemoteExtensionCLIManagementService); + + const lines: string[] = []; + const output = { log: lines.push.bind(lines), error: lines.push.bind(lines) }; + + if (args.list) { + await cliService.listExtensions(!!args.list.showVersions, args.list.category, output); + } else { + const revive = (inputs: (string | UriComponents)[]) => inputs.map(input => isString(input) ? input : URI.revive(input)); + if (Array.isArray(args.install) && args.install.length) { + try { + await cliService.installExtensions(revive(args.install), [], true, !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + if (Array.isArray(args.uninstall) && args.uninstall.length) { + try { + await cliService.uninstallExtensions(revive(args.uninstall), !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + } + return lines.join('\n'); +}); + +class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService { + + private _location: string | undefined; + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService localizationsService: ILocalizationsService, + @ILabelService labelService: ILabelService, + @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService + ) { + super(extensionManagementService, extensionGalleryService, localizationsService); + + const remoteAuthority = envService.remoteAuthority; + this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined; + } + + protected get location(): string | undefined { + return this._location; + } + + protected validateExtensionKind(manifest: IExtensionManifest, output: CLIOutput): boolean { + if (!canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) { + output.log(localize('cannot be installed', "Cannot install the '{0}' extension because it is declared to not run in this setup.", getExtensionId(manifest.publisher, manifest.name))); + return false; + } + return true; + } +} diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index b03e809a9d..ff472991f1 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -473,7 +473,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments if (!commentsViewAlreadyRegistered) { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: COMMENTS_VIEW_ID, - name: COMMENTS_VIEW_TITLE, + title: COMMENTS_VIEW_TITLE, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: COMMENTS_VIEW_TITLE, hideIfEmpty: true, diff --git a/src/vs/workbench/api/browser/mainThreadConsole.ts b/src/vs/workbench/api/browser/mainThreadConsole.ts index 69c18adfde..01e70ae8fd 100644 --- a/src/vs/workbench/api/browser/mainThreadConsole.ts +++ b/src/vs/workbench/api/browser/mainThreadConsole.ts @@ -10,22 +10,18 @@ import { IRemoteConsoleLog, log } from 'vs/base/common/console'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @extHostNamedCustomer(MainContext.MainThreadConsole) export class MainThreadConsole implements MainThreadConsoleShape { - private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevTestFromCli: boolean; constructor( - extHostContext: IExtHostContext, + _extHostContext: IExtHostContext, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ILogService private readonly _logService: ILogService, - @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, ) { const devOpts = parseExtensionDevOptions(this._environmentService); - this._isExtensionDevHost = devOpts.isExtensionDevHost; this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli; } @@ -43,10 +39,5 @@ export class MainThreadConsole implements MainThreadConsoleShape { if (this._isExtensionDevTestFromCli) { logRemoteEntry(this._logService, entry); } - - // Broadcast to other windows if we are in development mode - else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { - this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); - } } } diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 29aa534575..7984cfa41d 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -3,15 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { multibyteAwareBtoa } from 'vs/base/browser/dom'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources'; -import { multibyteAwareBtoa } from 'vs/base/browser/dom'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; @@ -95,10 +95,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc dispose() { super.dispose(); - for (const disposable of this._editorProviders.values()) { - disposable.dispose(); - } - + dispose(this._editorProviders.values()); this._editorProviders.clear(); } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 11c9f57c6f..3e83b7f15f 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -75,8 +75,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.resolve(this._proxy.$substituteVariables(folder ? folder.uri : undefined, config)); } - runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise { - return this._proxy.$runInTerminal(args); + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise { + return this._proxy.$runInTerminal(args, sessionId); } // RPC methods (MainThreadDebugServiceShape) @@ -327,7 +327,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return { id: sessionID, type: session.configuration.type, - name: session.configuration.name, + name: session.name, folderUri: session.root ? session.root.uri : undefined, configuration: session.configuration }; diff --git a/src/vs/workbench/api/browser/mainThreadDialogs.ts b/src/vs/workbench/api/browser/mainThreadDialogs.ts index 7b1f8b5067..dd1e29dca9 100644 --- a/src/vs/workbench/api/browser/mainThreadDialogs.ts +++ b/src/vs/workbench/api/browser/mainThreadDialogs.ts @@ -23,12 +23,20 @@ export class MainThreadDialogs implements MainThreadDiaglogsShape { // } - $showOpenDialog(options?: MainThreadDialogOpenOptions): Promise { - return Promise.resolve(this._fileDialogService.showOpenDialog(MainThreadDialogs._convertOpenOptions(options))); + async $showOpenDialog(options?: MainThreadDialogOpenOptions): Promise { + const convertedOptions = MainThreadDialogs._convertOpenOptions(options); + if (!convertedOptions.defaultUri) { + convertedOptions.defaultUri = await this._fileDialogService.defaultFilePath(); + } + return Promise.resolve(this._fileDialogService.showOpenDialog(convertedOptions)); } - $showSaveDialog(options?: MainThreadDialogSaveOptions): Promise { - return Promise.resolve(this._fileDialogService.showSaveDialog(MainThreadDialogs._convertSaveOptions(options))); + async $showSaveDialog(options?: MainThreadDialogSaveOptions): Promise { + const convertedOptions = MainThreadDialogs._convertSaveOptions(options); + if (!convertedOptions.defaultUri) { + convertedOptions.defaultUri = await this._fileDialogService.defaultFilePath(); + } + return Promise.resolve(this._fileDialogService.showSaveDialog(convertedOptions)); } private static _convertOpenOptions(options?: MainThreadDialogOpenOptions): IOpenDialogOptions { diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 1f2fec5f86..ce57ef3345 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -79,7 +79,6 @@ export class MainThreadTextEditorProperties { return { insertSpaces: modelOptions.insertSpaces, tabSize: modelOptions.tabSize, - indentSize: modelOptions.indentSize, cursorStyle: cursorStyle, lineNumbers: lineNumbers }; @@ -146,7 +145,6 @@ export class MainThreadTextEditorProperties { } return ( a.tabSize === b.tabSize - && a.indentSize === b.indentSize && a.insertSpaces === b.insertSpaces && a.cursorStyle === b.cursorStyle && a.lineNumbers === b.lineNumbers @@ -377,13 +375,6 @@ export class MainThreadTextEditor { if (typeof newConfiguration.tabSize !== 'undefined') { newOpts.tabSize = newConfiguration.tabSize; } - if (typeof newConfiguration.indentSize !== 'undefined') { - if (newConfiguration.indentSize === 'tabSize') { - newOpts.indentSize = newOpts.tabSize || creationOpts.tabSize; - } else { - newOpts.indentSize = newConfiguration.indentSize; - } - } this._model.updateOptions(newOpts); } diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts new file mode 100644 index 0000000000..eaff99cfc5 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { Verbosity } from 'vs/workbench/common/editor'; +import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; + +export interface ITabInfo { + name: string; + resource: URI; +} + +@extHostNamedCustomer(MainContext.MainThreadEditorTabs) +export class MainThreadEditorTabs { + + private static _GroupEventFilter = new Set([GroupChangeKind.EDITOR_CLOSE, GroupChangeKind.EDITOR_OPEN]); + + private readonly _dispoables = new DisposableStore(); + private readonly _groups = new Map(); + private readonly _proxy: IExtHostEditorTabsShape; + + constructor( + extHostContext: IExtHostContext, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + ) { + + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); + + this._editorGroupsService.groups.forEach(this._subscribeToGroup, this); + this._dispoables.add(_editorGroupsService.onDidAddGroup(this._subscribeToGroup, this)); + this._dispoables.add(_editorGroupsService.onDidRemoveGroup(e => { + const subscription = this._groups.get(e); + if (subscription) { + subscription.dispose(); + this._groups.delete(e); + this._pushEditorTabs(); + } + })); + this._pushEditorTabs(); + } + + dispose(): void { + dispose(this._groups.values()); + this._dispoables.dispose(); + } + + private _subscribeToGroup(group: IEditorGroup) { + this._groups.get(group)?.dispose(); + const listener = group.onDidGroupChange(e => { + if (MainThreadEditorTabs._GroupEventFilter.has(e.kind)) { + this._pushEditorTabs(); + } + }); + this._groups.set(group, listener); + } + + private _pushEditorTabs(): void { + const tabs: IEditorTabDto[] = []; + for (const group of this._editorGroupsService.groups) { + for (const editor of group.editors) { + if (editor.isDisposed() || !editor.resource) { + continue; + } + tabs.push({ + group: group.id, + name: editor.getTitle(Verbosity.SHORT) ?? '', + resource: editor.resource + }); + } + } + + this._proxy.$acceptEditorTabs(tabs); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 67e617fe77..518bc6ca60 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -27,7 +27,7 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo import { revive } from 'vs/base/common/marshalling'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { +export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { return []; } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index d7b9ec8691..443209224b 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -7,7 +7,7 @@ import { SerializedError } from 'vs/base/common/errors'; import Severity from 'vs/base/common/severity'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IExtHostContext, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtensionService, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; @@ -19,29 +19,23 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { CancellationToken } from 'vs/base/common/cancellation'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { - private readonly _extensionService: IExtensionService; - private readonly _notificationService: INotificationService; - private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService; - private readonly _hostService: IHostService; - private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService; + private readonly _extensionHostKind: ExtensionHostKind; constructor( extHostContext: IExtHostContext, - @IExtensionService extensionService: IExtensionService, - @INotificationService notificationService: INotificationService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @IHostService hostService: IHostService, - @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService + @IExtensionService private readonly _extensionService: IExtensionService, + @INotificationService private readonly _notificationService: INotificationService, + @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService, + @IHostService private readonly _hostService: IHostService, + @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, + @ITimerService private readonly _timerService: ITimerService, ) { - this._extensionService = extensionService; - this._notificationService = notificationService; - this._extensionsWorkbenchService = extensionsWorkbenchService; - this._hostService = hostService; - this._extensionEnablementService = extensionEnablementService; + this._extensionHostKind = extHostContext.extensionHostKind; } public dispose(): void { @@ -131,4 +125,14 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha async $onExtensionHostExit(code: number): Promise { this._extensionService._onExtensionHostExit(code); } + + async $setPerformanceMarks(marks: PerformanceMark[]): Promise { + if (this._extensionHostKind === ExtensionHostKind.LocalProcess) { + this._timerService.setPerformanceMarks('localExtHost', marks); + } else if (this._extensionHostKind === ExtensionHostKind.LocalWebWorker) { + this._timerService.setPerformanceMarks('workerExtHost', marks); + } else { + this._timerService.setPerformanceMarks('remoteExtHost', marks); + } + } } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 77c170b080..2c1dc18124 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -4,23 +4,43 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileOperation, IFileService } from 'vs/platform/files/common/files'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol'; import { localize } from 'vs/nls'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @extHostCustomer export class MainThreadFileSystemEventService { + static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`; + private readonly _listener = new DisposableStore(); constructor( extHostContext: IExtHostContext, @IFileService fileService: IFileService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IBulkEditService bulkEditService: IBulkEditService, + @IProgressService progressService: IProgressService, + @IDialogService dialogService: IDialogService, + @IStorageService storageService: IStorageService, + @ILogService logService: ILogService, + @IEnvironmentService envService: IEnvironmentService ) { const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); @@ -53,14 +73,124 @@ export class MainThreadFileSystemEventService { })); - // BEFORE file operation - this._listener.add(workingCopyFileService.addFileOperationParticipant({ - participate: async (files, operation, undoRedoGroupId, isUndoing, _progress, timeout, token) => { - if (!isUndoing) { - return proxy.$onWillRunFileOperation(operation, files, undoRedoGroupId, timeout, token); + const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant { + async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) { + if (undoInfo?.isUndoing) { + return; + } + + const cts = new CancellationTokenSource(token); + const timer = setTimeout(() => cts.cancel(), timeout); + + const data = await progressService.withProgress({ + location: ProgressLocation.Notification, + title: this._progressLabel(operation), + cancellable: true, + delay: Math.min(timeout / 2, 3000) + }, () => { + // race extension host event delivery against timeout AND user-cancel + const onWillEvent = proxy.$onWillRunFileOperation(operation, files, timeout, token); + return raceCancellation(onWillEvent, cts.token); + }, () => { + // user-cancel + cts.cancel(); + + }).finally(() => { + cts.dispose(); + clearTimeout(timer); + }); + + if (!data) { + // cancelled or no reply + return; + } + + const needsConfirmation = data.edit.edits.some(edit => edit.metadata?.needsConfirmation); + let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + + if (envService.extensionTestsLocationURI) { + // don't show dialog in tests + showPreview = false; + } + + if (showPreview === undefined) { + // show a user facing message + + let message: string; + if (data.extensionNames.length === 1) { + if (operation === FileOperation.CREATE) { + message = localize('ask.1.create', "Extension '{0}' wants to make refactoring changes with this file creation", data.extensionNames[0]); + } else if (operation === FileOperation.COPY) { + message = localize('ask.1.copy', "Extension '{0}' wants to make refactoring changes with this file copy", data.extensionNames[0]); + } else if (operation === FileOperation.MOVE) { + message = localize('ask.1.move', "Extension '{0}' wants to make refactoring changes with this file move", data.extensionNames[0]); + } else /* if (operation === FileOperation.DELETE) */ { + message = localize('ask.1.delete', "Extension '{0}' wants to make refactoring changes with this file deletion", data.extensionNames[0]); + } + } else { + if (operation === FileOperation.CREATE) { + message = localize('ask.N.create', "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length); + } else if (operation === FileOperation.COPY) { + message = localize('ask.N.copy', "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length); + } else if (operation === FileOperation.MOVE) { + message = localize('ask.N.move', "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length); + } else /* if (operation === FileOperation.DELETE) */ { + message = localize('ask.N.delete', "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length); + } + } + + if (needsConfirmation) { + // edit which needs confirmation -> always show dialog + const answer = await dialogService.show(Severity.Info, message, [localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], { cancelId: 1 }); + showPreview = true; + if (answer.choice === 1) { + // no changes wanted + return; + } + } else { + // choice + const answer = await dialogService.show(Severity.Info, message, + [localize('ok', "OK"), localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], + { + cancelId: 2, + checkbox: { label: localize('again', "Don't ask again") } + } + ); + if (answer.choice === 2) { + // no changes wanted, don't persist cancel option + return; + } + showPreview = answer.choice === 1; + if (answer.checkboxChecked /* && answer.choice !== 2 */) { + storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.GLOBAL, StorageTarget.USER); + } + } + } + + logService.info('[onWill-handler] applying additional workspace edit from extensions', data.extensionNames); + + await bulkEditService.apply( + reviveWorkspaceEditDto2(data.edit), + { undoRedoGroupId: undoInfo?.undoRedoGroupId, showPreview } + ); + } + + private _progressLabel(operation: FileOperation): string { + switch (operation) { + case FileOperation.CREATE: + return localize('msg-create', "Running 'File Create' participants..."); + case FileOperation.MOVE: + return localize('msg-rename', "Running 'File Rename' participants..."); + case FileOperation.COPY: + return localize('msg-copy', "Running 'File Copy' participants..."); + case FileOperation.DELETE: + return localize('msg-delete', "Running 'File Delete' participants..."); } } - })); + }; + + // BEFORE file operation + this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant)); // AFTER file operation this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files))); @@ -71,6 +201,19 @@ export class MainThreadFileSystemEventService { } } +registerAction2(class ResetMemento extends Action2 { + constructor() { + super({ + id: 'files.participants.resetChoice', + title: localize('label', "Reset choice for 'File operation needs preview'"), + f1: true + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + } +}); + Registry.as(Extensions.Configuration).registerConfiguration({ id: 'files', diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index d168481876..c07c0eabd6 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -21,7 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -498,6 +498,32 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- inline hints + + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { + provideInlineHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { + const result = await this._proxy.$provideInlineHints(handle, model.uri, range, token); + return result?.hints; + } + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChangeInlineHints = emitter.event; + } + + this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider)); + } + + $emitInlineHintsEvent(eventHandle: number, event?: any): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } + } + // --- links $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void { @@ -663,7 +689,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { beforeText: MainThreadLanguageFeatures._reviveRegExp(onEnterRule.beforeText), afterText: onEnterRule.afterText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.afterText) : undefined, - oneLineAboveText: onEnterRule.oneLineAboveText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.oneLineAboveText) : undefined, + previousLineText: onEnterRule.previousLineText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.previousLineText) : undefined, action: onEnterRule.action }; } @@ -702,7 +728,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha const languageIdentifier = this._modeService.getLanguageIdentifier(languageId); if (languageIdentifier) { - this._registrations.set(handle, LanguageConfigurationRegistry.register(languageIdentifier, configuration)); + this._registrations.set(handle, LanguageConfigurationRegistry.register(languageIdentifier, configuration, 100)); } } diff --git a/src/vs/workbench/api/browser/mainThreadMessageService.ts b/src/vs/workbench/api/browser/mainThreadMessageService.ts index 196f40dba0..13de4916b6 100644 --- a/src/vs/workbench/api/browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/browser/mainThreadMessageService.ts @@ -33,7 +33,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { $showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise { if (options.modal) { - return this._showModalMessage(severity, message, commands); + return this._showModalMessage(severity, message, commands, options.useCustom); } else { return this._showMessage(severity, message, commands, options.extension); } @@ -97,7 +97,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { }); } - private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise { + private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], useCustom?: boolean): Promise { let cancelId: number | undefined = undefined; const buttons = commands.map((command, index) => { @@ -118,7 +118,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { cancelId = buttons.length - 1; } - const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId }); + const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId, useCustom }); return choice === commands.length ? undefined : commands[choice].handle; } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 80dbeb25d5..e133d9cfa7 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -10,14 +10,13 @@ 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 { Schemas } from 'vs/base/common/network'; import { IExtUri } 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'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { viewColumnToEditorGroup } from 'vs/workbench/common/editor'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -31,7 +30,6 @@ 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 { @@ -129,12 +127,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IQuickInputService private readonly quickInputService: IQuickInputService, @ILogService private readonly logService: ILogService, @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService, - @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); @@ -605,32 +602,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return false; } - $onUndoableContentChange(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void { - const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - - if (textModel) { - textModel.handleUnknownUndoableEdit(label, () => { - const isDirty = this._workingCopyService.isDirty(textModel.uri.with({ scheme: Schemas.vscodeNotebook })); - return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, isDirty); - }, () => { - const isDirty = this._workingCopyService.isDirty(textModel.uri.with({ scheme: Schemas.vscodeNotebook })); - return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, isDirty); - }); - } - } - - $onContentChange(resource: UriComponents, viewType: string): void { - const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - - if (textModel) { - textModel.applyEdits(textModel.versionId, [ - { - editType: CellEditType.Unknown - } - ], true, undefined, () => undefined, undefined); - } - } - async $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType) { const editor = this._notebookService.listNotebookEditors().find(editor => editor.getId() === id); if (editor && editor.isNotebookEditor) { @@ -646,14 +617,13 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo switch (revealType) { case NotebookEditorRevealType.Default: - notebookEditor.revealInView(cell); - break; + return notebookEditor.revealCellRangeInView(range); case NotebookEditorRevealType.InCenter: - notebookEditor.revealInCenter(cell); - break; + return notebookEditor.revealInCenter(cell); case NotebookEditorRevealType.InCenterIfOutsideViewport: - notebookEditor.revealInCenterIfOutsideViewport(cell); - break; + return notebookEditor.revealInCenterIfOutsideViewport(cell); + case NotebookEditorRevealType.AtTop: + return notebookEditor.revealInViewAtTop(cell); default: break; } @@ -733,7 +703,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const input = this.editorService.createEditorInput({ resource: URI.revive(resource), options: editorOptions }); // TODO: handle options.selection - const editorPane = await openEditorWith(input, viewType, options, group, this.editorService, this.configurationService, this.quickInputService); + const editorPane = await this._instantiationService.invokeFunction(openEditorWith, input, viewType, options, group); const notebookEditor = (editorPane as unknown as { isNotebookEditor?: boolean })?.isNotebookEditor ? (editorPane!.getControl() as INotebookEditor) : undefined; if (notebookEditor) { diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts new file mode 100644 index 0000000000..6c7a76ee49 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; +import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; +import { IExtHostContext, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol'; + +@extHostNamedCustomer(MainContext.MainThreadSecretState) +export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape { + // private readonly _proxy: ExtHostSecretStateShape; + + constructor( + extHostContext: IExtHostContext, + @ICredentialsService private readonly credentialsService: ICredentialsService, + @IEncryptionService private readonly encryptionService: IEncryptionService, + @IProductService private readonly productService: IProductService + ) { + super(); + // this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState); + + // {{SQL CARBON EDIT}} - throws null + /* this._register(this.credentialsService.onDidChangePassword(e => { + const extensionId = e.service.substring(this.productService.urlProtocol.length); + this._proxy.$onDidChangePassword({ extensionId, key: e.account }); + })); */ + } + + private getFullKey(extensionId: string): string { + return `${this.productService.urlProtocol}${extensionId}`; + } + + async $getPassword(extensionId: string, key: string): Promise { + const fullKey = this.getFullKey(extensionId); + const password = await this.credentialsService.getPassword(fullKey, key); + const decrypted = password && await this.encryptionService.decrypt(password); + + if (decrypted) { + try { + const value = JSON.parse(decrypted); + if (value.extensionId === extensionId) { + return value.content; + } + } catch (_) { + throw new Error('Cannot get password'); + } + } + + return undefined; + } + + async $setPassword(extensionId: string, key: string, value: string): Promise { + const fullKey = this.getFullKey(extensionId); + const toEncrypt = JSON.stringify({ + extensionId, + content: value + }); + const encrypted = await this.encryptionService.encrypt(toEncrypt); + return this.credentialsService.setPassword(fullKey, key, encrypted); + } + + async $deletePassword(extensionId: string, key: string): Promise { + try { + const fullKey = this.getFullKey(extensionId); + await this.credentialsService.deletePassword(fullKey, key); + } catch (_) { + throw new Error('Cannot delete password'); + } + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 5d9f41e333..32448e12bc 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -567,13 +567,19 @@ export class MainThreadTask implements MainThreadTaskShape { if (!task) { reject(new Error('Task not found')); } else { - this._taskService.run(task).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); const result: TaskExecutionDTO = { id: value.id, task: TaskDTO.from(task) }; + this._taskService.run(task).then(summary => { + // Ensure that the task execution gets cleaned up if the exit code is undefined + // This can happen when the task has dependent tasks and one of them failed + if ((summary?.exitCode === undefined) || (summary.exitCode !== 0)) { + this._proxy.$OnDidEndTask(result); + } + }, reason => { + // eat the error, it has already been surfaced to the user and we don't care about it here + }); resolve(result); } }, (_error) => { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 6326528c35..82937244b4 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -16,11 +16,18 @@ import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/termi import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { private _proxy: ExtHostTerminalServiceShape; + /** + * Stores a map from a temporary terminal id (a UUID generated on the extension host side) + * to a numeric terminal id (an id generated on the renderer side) + * This comes in play only when dealing with terminals created on the extension host side + */ + private _extHostTerminalIds = new Map(); private _remoteAuthority: string | null; private readonly _toDispose = new DisposableStore(); private readonly _terminalProcessProxies = new Map(); @@ -40,6 +47,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private readonly _logService: ILogService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); @@ -47,13 +55,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // ITerminalService listeners this._toDispose.add(_terminalService.onInstanceCreated((instance) => { - // Delay this message so the TerminalInstance constructor has a chance to finish and - // return the ID normally to the extension host. The ID that is passed here will be - // used to register non-extension API terminals in the extension host. - setTimeout(() => { - this._onTerminalOpened(instance); - this._onInstanceDimensionsChanged(instance); - }, EXT_HOST_CREATION_DELAY); + this._onTerminalOpened(instance); + this._onInstanceDimensionsChanged(instance); })); this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); @@ -100,7 +103,22 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape // when the extension host process goes down ? } - public $createTerminal(launchConfig: TerminalLaunchConfig): Promise<{ id: number, name: string }> { + private _getTerminalId(id: TerminalIdentifier): number | undefined { + if (typeof id === 'number') { + return id; + } + return this._extHostTerminalIds.get(id); + } + + private _getTerminalInstance(id: TerminalIdentifier): ITerminalInstance | undefined { + const rendererId = this._getTerminalId(id); + if (typeof rendererId === 'number') { + return this._terminalService.getInstanceFromId(rendererId); + } + return undefined; + } + + public async $createTerminal(extHostTerminalId: string, launchConfig: TerminalLaunchConfig): Promise { const shellLaunchConfig: IShellLaunchConfig = { name: launchConfig.name, executable: launchConfig.shellPath, @@ -112,39 +130,38 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape strictEnv: launchConfig.strictEnv, hideFromUser: launchConfig.hideFromUser, isExtensionTerminal: launchConfig.isExtensionTerminal, + extHostTerminalId: extHostTerminalId, isFeatureTerminal: launchConfig.isFeatureTerminal }; const terminal = this._terminalService.createTerminal(shellLaunchConfig); - return Promise.resolve({ - id: terminal.id, - name: terminal.title - }); + this._extHostTerminalIds.set(extHostTerminalId, terminal.id); } - public $show(terminalId: number, preserveFocus: boolean): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $show(id: TerminalIdentifier, preserveFocus: boolean): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { this._terminalService.setActiveInstance(terminalInstance); this._terminalService.showPanel(!preserveFocus); } } - public $hide(terminalId: number): void { + public $hide(id: TerminalIdentifier): void { + const rendererId = this._getTerminalId(id); const instance = this._terminalService.getActiveInstance(); - if (instance && instance.id === terminalId) { + if (instance && instance.id === rendererId) { this._terminalService.hidePanel(); } } - public $dispose(terminalId: number): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $dispose(id: TerminalIdentifier): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { terminalInstance.dispose(); } } - public $sendText(terminalId: number, text: string, addNewLine: boolean): void { - const terminalInstance = this._terminalService.getInstanceFromId(terminalId); + public $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void { + const terminalInstance = this._getTerminalInstance(id); if (terminalInstance) { terminalInstance.sendText(text, addNewLine); } @@ -204,6 +221,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } private _onTerminalOpened(terminalInstance: ITerminalInstance): void { + const extHostTerminalId = terminalInstance.shellLaunchConfig.extHostTerminalId; const shellLaunchConfigDto: IShellLaunchConfigDto = { name: terminalInstance.shellLaunchConfig.name, executable: terminalInstance.shellLaunchConfig.executable, @@ -212,13 +230,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser }; - if (terminalInstance.title) { - this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto); - } else { - terminalInstance.waitForTitle().then(title => { - this._proxy.$acceptTerminalOpened(terminalInstance.id, title, shellLaunchConfigDto); - }); - } + this._proxy.$acceptTerminalOpened(terminalInstance.id, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void { @@ -249,7 +261,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape executable: request.shellLaunchConfig.executable, args: request.shellLaunchConfig.args, cwd: request.shellLaunchConfig.cwd, - env: request.shellLaunchConfig.env + env: request.shellLaunchConfig.env, + flowControl: this._configurationService.getValue(TERMINAL_CONFIG_SECTION).flowControl }; this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request }); @@ -260,8 +273,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape request.cols, request.rows, request.isWorkspaceShellAllowed - ).then(request.callback); + ).then(request.callback, request.callback); + proxy.onAcknowledgeDataEvent(charCount => this._proxy.$acceptProcessAckDataEvent(proxy.terminalId, charCount)); proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data)); proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows)); proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate)); @@ -294,38 +308,59 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $sendProcessTitle(terminalId: number, title: string): void { - this._getTerminalProcess(terminalId).emitTitle(title); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitTitle(title); + } } public $sendProcessData(terminalId: number, data: string): void { - this._getTerminalProcess(terminalId).emitData(data); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitData(data); + } } public $sendProcessReady(terminalId: number, pid: number, cwd: string): void { - this._getTerminalProcess(terminalId).emitReady(pid, cwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitReady(pid, cwd); + } } public $sendProcessExit(terminalId: number, exitCode: number | undefined): void { - this._getTerminalProcess(terminalId).emitExit(exitCode); - this._terminalProcessProxies.delete(terminalId); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitExit(exitCode); + this._terminalProcessProxies.delete(terminalId); + } } public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void { - this._getTerminalProcess(terminalId).emitOverrideDimensions(dimensions); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitOverrideDimensions(dimensions); + } } public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void { - this._getTerminalProcess(terminalId).emitInitialCwd(initialCwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitInitialCwd(initialCwd); + } } public $sendProcessCwd(terminalId: number, cwd: string): void { - this._getTerminalProcess(terminalId).emitCwd(cwd); + const terminalProcess = this._terminalProcessProxies.get(terminalId); + if (terminalProcess) { + terminalProcess.emitCwd(cwd); + } } public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void { const instance = this._terminalService.getInstanceFromId(terminalId); if (instance) { - this._getTerminalProcess(terminalId).emitResolvedShellLaunchConfig(shellLaunchConfig); + this._getTerminalProcess(terminalId)?.emitResolvedShellLaunchConfig(shellLaunchConfig); } } @@ -338,7 +373,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape sw.stop(); sum += sw.elapsed(); } - this._getTerminalProcess(terminalId).emitLatency(sum / COUNT); + this._getTerminalProcess(terminalId)?.emitLatency(sum / COUNT); } private _isPrimaryExtHost(): boolean { @@ -363,10 +398,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } - private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy { + private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy | undefined { const terminal = this._terminalProcessProxies.get(terminalId); if (!terminal) { - throw new Error(`Unknown terminal: ${terminalId}`); + this._logService.error(`Unknown terminal: ${terminalId}`); + return undefined; } return terminal; } diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index f2b239a804..a4995834fc 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -3,12 +3,31 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; + +const reviveDiff = (diff: TestsDiff) => { + for (const entry of diff) { + if (entry[0] === TestDiffOpType.Add || entry[0] === TestDiffOpType.Update) { + const item = entry[1]; + if (item.item.location) { + item.item.location.uri = URI.revive(item.item.location.uri); + } + + for (const message of item.item.state.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + } + } + } + } +}; @extHostNamedCustomer(MainContext.MainThreadTesting) export class MainThreadTesting extends Disposable implements MainThreadTestingShape { @@ -18,11 +37,28 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh constructor( extHostContext: IExtHostContext, @ITestService private readonly testService: ITestService, + @ITestResultService resultService: ITestResultService, ) { super(); this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting); this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri))); this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri))); + + const testCompleteListener = this._register(new MutableDisposable()); + this._register(resultService.onNewTestResult(results => { + testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests })); + })); + + testService.updateRootProviderCount(1); + + const lastCompleted = resultService.results.find(r => !r.isComplete); + if (lastCompleted) { + this.proxy.$publishTestResults({ tests: lastCompleted.tests }); + } + + for (const { resource, uri } of this.testService.subscriptions) { + this.proxy.$subscribeToTests(resource, uri); + } } /** @@ -30,7 +66,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh */ public $registerTestProvider(id: string) { this.testService.registerTestController(id, { - runTests: req => this.proxy.$runTestsForProvider(req), + runTests: (req, token) => this.proxy.$runTestsForProvider(req, token), + lookupTest: test => this.proxy.$lookupTest(test), }); } @@ -64,14 +101,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh * @inheritdoc */ public $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void { + reviveDiff(diff); this.testService.publishDiff(resource, URI.revive(uri), diff); } - public $runTests(req: RunTestsRequest): Promise { - return this.testService.runTests(req); + public $runTests(req: RunTestsRequest, token: CancellationToken): Promise { + return this.testService.runTests(req, token); } public dispose() { - // no-op + this.testService.updateRootProviderCount(-1); + for (const subscription of this.testSubscriptions.values()) { + subscription.dispose(); + } + this.testSubscriptions.clear(); } } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 8f22e1d759..47f49ff75f 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -33,7 +33,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): void { + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean; }): void { this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options); this.extensionService.whenInstalledExtensionsRegistered().then(() => { @@ -48,6 +48,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie viewer.dataProvider = dataProvider; this.registerListeners(treeViewId, viewer); this._proxy.$setVisible(treeViewId, viewer.visible); + // {{SQL CARBON EDIT}} } else if (treeViewId.includes('connectionDialog')) { this.connectionTreeService.registerTreeProvider(treeViewId, dataProvider); } else { @@ -56,7 +57,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie }); } - $reveal(treeViewId: string, itemInfo: { item: ITreeItem, parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise { + $reveal(treeViewId: string, itemInfo: { item: ITreeItem, parentChain: ITreeItem[]; } | undefined, options: IRevealOptions): Promise { this.logService.trace('MainThreadTreeViews#$reveal', treeViewId, itemInfo?.item, itemInfo?.parentChain, options); return this.viewsService.openView(treeViewId, options.focus) @@ -69,7 +70,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie }); } - $refresh(treeViewId: string, itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): Promise { + $refresh(treeViewId: string, itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem; }): Promise { this.logService.trace('MainThreadTreeViews#$refresh', treeViewId, itemsToRefreshByHandle); const viewer = this.getTreeView(treeViewId); @@ -77,6 +78,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie if (viewer && dataProvider) { const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle); return viewer.refresh(itemsToRefresh.length ? itemsToRefresh : undefined); + // {{SQL CARBON EDIT}} } else if (treeViewId.includes('connectionDialog')) { const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle); return this.connectionTreeService.view?.refresh(itemsToRefresh.length ? itemsToRefresh : undefined); @@ -193,7 +195,7 @@ export class TreeViewDataProvider implements ITreeViewDataProvider { })); } - getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] { + getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem; }): ITreeItem[] { const itemsToRefresh: ITreeItem[] = []; if (itemsToRefreshByHandle) { for (const treeItemHandle of Object.keys(itemsToRefreshByHandle)) { @@ -235,8 +237,8 @@ export class TreeViewDataProvider implements ITreeViewDataProvider { // {{SQL CARBON EDIT}} We rely on custom properties on the tree items in a number of places so creating a new item here was // {{SQL CARBON EDIT}} clearing those. Revert to old behavior if the provider doesn't support a resolve (our normal case) if (hasResolve) { - const resolvable = new ResolvableTreeItem(element, hasResolve ? () => { - return this._proxy.$resolve(this.treeViewId, element.handle); + const resolvable = new ResolvableTreeItem(element, hasResolve ? (token) => { + return this._proxy.$resolve(this.treeViewId, element.handle, token); } : undefined); this.itemsMap.set(element.handle, resolvable); result.push(resolvable); @@ -252,10 +254,14 @@ export class TreeViewDataProvider implements ITreeViewDataProvider { private updateTreeItem(current: ITreeItem, treeItem: ITreeItem): void { treeItem.children = treeItem.children ? treeItem.children : undefined; if (current) { - const properties = distinct([...Object.keys(current), ...Object.keys(treeItem)]); + const properties = distinct([...Object.keys(current instanceof ResolvableTreeItem ? current.asTreeItem() : current), + ...Object.keys(treeItem)]); for (const property of properties) { (current)[property] = (treeItem)[property]; } + if (current instanceof ResolvableTreeItem) { + current.resetResolve(); + } } } } diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index e244877f11..8c15ab72c6 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -3,22 +3,31 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { CandidatePort, IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { PORT_AUTO_FORWARD_SETTING } from 'vs/workbench/contrib/remote/browser/tunnelView'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape { private readonly _proxy: ExtHostTunnelServiceShape; + private elevateionRetry: boolean = false; constructor( extHostContext: IExtHostContext, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @ITunnelService private readonly tunnelService: ITunnelService + @ITunnelService private readonly tunnelService: ITunnelService, + @INotificationService private readonly notificationService: INotificationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService); @@ -26,14 +35,50 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun this._register(tunnelService.onTunnelClosed(() => this._proxy.$onDidTunnelsChange())); } + async $setCandidateFinder(): Promise { + if (this.remoteExplorerService.portsFeaturesEnabled) { + this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)); + } else { + this._register(this.remoteExplorerService.onEnabledPortsFeatures(() => this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)))); + } + this._register(this.configurationService.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) { + return this._proxy.$registerCandidateFinder((this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING))); + } + })); + } + async $openTunnel(tunnelOptions: TunnelOptions, source: string): Promise { - const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source); + const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, false); if (tunnel) { + if (!this.elevateionRetry + && (tunnelOptions.localAddressPort !== undefined) + && (tunnel.tunnelLocalPort !== undefined) + && isPortPrivileged(tunnelOptions.localAddressPort) + && (tunnel.tunnelLocalPort !== tunnelOptions.localAddressPort) + && this.tunnelService.canElevate) { + + this.elevationPrompt(tunnelOptions, tunnel, source); + } return TunnelDto.fromServiceTunnel(tunnel); } return undefined; } + private async elevationPrompt(tunnelOptions: TunnelOptions, tunnel: RemoteTunnel, source: string) { + return this.notificationService.prompt(Severity.Info, + nls.localize('remote.tunnel.openTunnel', "The extension {0} has forwarded port {1}. You'll need to run as superuser to use port {2} locally.", source, tunnelOptions.remoteAddress.port, tunnelOptions.localAddressPort), + [{ + label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort), + run: async () => { + this.elevateionRetry = true; + await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, true); + this.elevateionRetry = false; + } + }]); + } + async $closeTunnel(remote: { host: string, port: number }): Promise { return this.remoteExplorerService.close(remote); } @@ -47,27 +92,29 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun }); } - async $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise { + async $onFoundNewCandidates(candidates: CandidatePort[]): Promise { this.remoteExplorerService.onFoundNewCandidates(candidates); } - async $tunnelServiceReady(): Promise { - return this.remoteExplorerService.restore(); - } - - async $setTunnelProvider(): Promise { + async $setTunnelProvider(features: TunnelProviderFeatures): Promise { const tunnelProvider: ITunnelProvider = { forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => { const forward = this._proxy.$forwardPort(tunnelOptions, tunnelCreationOptions); if (forward) { return forward.then(tunnel => { + this.logService.trace(`MainThreadTunnelService: New tunnel established by tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`); + if (!tunnel) { + return undefined; + } return { tunnelRemotePort: tunnel.remoteAddress.port, tunnelRemoteHost: tunnel.remoteAddress.host, localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined, - dispose: (silent?: boolean) => { - this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); + public: tunnel.public, + dispose: async (silent?: boolean) => { + this.logService.trace(`MainThreadTunnelService: Closing tunnel from tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`); + return this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); } }; }); @@ -75,9 +122,16 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun return undefined; } }; - this.tunnelService.setTunnelProvider(tunnelProvider); + this.tunnelService.setTunnelProvider(tunnelProvider, features); } + async $setCandidateFilter(): Promise { + this.remoteExplorerService.setCandidateFilter((candidates: CandidatePort[]): Promise => { + return this._proxy.$applyCandidateFilter(candidates); + }); + } + + dispose(): void { } diff --git a/src/vs/workbench/api/browser/mainThreadUriOpeners.ts b/src/vs/workbench/api/browser/mainThreadUriOpeners.ts new file mode 100644 index 0000000000..c4d4d1fd99 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadUriOpeners.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'vs/base/common/actions'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { defaultExternalUriOpenerId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { ContributedExternalUriOpenersStore } from 'vs/workbench/contrib/externalUriOpener/common/contributedOpeners'; +import { IExternalOpenerProvider, IExternalUriOpener, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { extHostNamedCustomer } from '../common/extHostCustomers'; + +interface RegisteredOpenerMetadata { + readonly schemes: ReadonlySet; + readonly extensionId: ExtensionIdentifier; + readonly label: string; +} + +@extHostNamedCustomer(MainContext.MainThreadUriOpeners) +export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpenersShape, IExternalOpenerProvider { + + private readonly proxy: ExtHostUriOpenersShape; + private readonly _registeredOpeners = new Map(); + private readonly _contributedExternalUriOpenersStore: ContributedExternalUriOpenersStore; + + constructor( + context: IExtHostContext, + @IStorageService storageService: IStorageService, + @IExternalUriOpenerService externalUriOpenerService: IExternalUriOpenerService, + @IExtensionService private readonly extensionService: IExtensionService, + @IOpenerService private readonly openerService: IOpenerService, + @INotificationService private readonly notificationService: INotificationService, + ) { + super(); + this.proxy = context.getProxy(ExtHostContext.ExtHostUriOpeners); + + this._register(externalUriOpenerService.registerExternalOpenerProvider(this)); + + this._contributedExternalUriOpenersStore = this._register(new ContributedExternalUriOpenersStore(storageService, extensionService)); + } + + public async *getOpeners(targetUri: URI): AsyncIterable { + + // Currently we only allow openers for http and https urls + if (targetUri.scheme !== Schemas.http && targetUri.scheme !== Schemas.https) { + return; + } + + await this.extensionService.activateByEvent(`onOpenExternalUri:${targetUri.scheme}`); + + for (const [id, openerMetadata] of this._registeredOpeners) { + if (openerMetadata.schemes.has(targetUri.scheme)) { + yield this.createOpener(id, openerMetadata); + } + } + } + + private createOpener(id: string, metadata: RegisteredOpenerMetadata): IExternalUriOpener { + return { + id: id, + label: metadata.label, + canOpen: (uri, token) => { + return this.proxy.$canOpenUri(id, uri, token); + }, + openExternalUri: async (uri, ctx, token) => { + try { + await this.proxy.$openUri(id, { resolvedUri: uri, sourceUri: ctx.sourceUri }, token); + } catch (e) { + if (!isPromiseCanceledError(e)) { + const openDefaultAction = new Action('default', localize('openerFailedUseDefault', "Open using default opener"), undefined, undefined, () => { + return this.openerService.open(uri, { + allowTunneling: false, + allowContributedOpeners: defaultExternalUriOpenerId, + }); + }); + openDefaultAction.tooltip = uri.toString(); + + this.notificationService.notify({ + severity: Severity.Error, + message: localize('openerFailedMessage', 'Could not open uri with \'{0}\': {1}', id, e.toString()), + actions: { + primary: [ + openDefaultAction + ] + } + }); + } + } + return true; + }, + }; + } + + async $registerUriOpener( + id: string, + schemes: readonly string[], + extensionId: ExtensionIdentifier, + label: string, + ): Promise { + if (this._registeredOpeners.has(id)) { + throw new Error(`Opener with id '${id}' already registered`); + } + + this._registeredOpeners.set(id, { + schemes: new Set(schemes), + label, + extensionId, + }); + + this._contributedExternalUriOpenersStore.didRegisterOpener(id, extensionId.value); + } + + async $unregisterUriOpener(id: string): Promise { + this._registeredOpeners.delete(id); + this._contributedExternalUriOpenersStore.delete(id); + } + + dispose(): void { + super.dispose(); + this._registeredOpeners.clear(); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 0109c03cab..e3bfe18376 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -129,6 +129,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc dispose(this._editorProviders.values()); this._editorProviders.clear(); + + dispose(this._revivers.values()); + this._revivers.clear(); } public get webviewInputs(): Iterable { return this._webviewInputs; } @@ -181,7 +184,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc webview.setName(value); } - public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { const webview = this.getWebviewInput(handle); webview.iconPath = reviveWebviewIcon(value); @@ -199,8 +201,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc } } - public $registerSerializer(viewType: string) - : void { + public $registerSerializer(viewType: string): void { if (this._revivers.has(viewType)) { throw new Error(`Reviver for ${viewType} already registered`); } @@ -216,7 +217,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc return; } - const handle = webviewInput.id; this.addWebviewInput(handle, webviewInput); diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 3a3f769bfb..fa94e3599e 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -52,7 +52,11 @@ export class MainThreadWindow implements MainThreadWindowShape { // called with URI or transformed -> use uri target = uri; } - return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling }); + return this.openerService.open(target, { + openExternal: true, + allowTunneling: options.allowTunneling, + allowContributedOpeners: options.allowContributedOpeners, + }); } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 5a5932f1b9..44cf7fd29d 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -6,25 +6,25 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isNative } from 'vs/base/common/platform'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { isNative } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; -import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IRequestService } from 'vs/platform/request/common/request'; -import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains'; +import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -139,7 +139,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } const query = this._queryBuilder.file( - includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders, + includeFolder ? [includeFolder] : workspace.folders, { maxResults: withNullAsUndefined(maxResults), disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined, diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index b4bc1abd28..8536f9deca 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -3,36 +3,31 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { forEach } from 'vs/base/common/collections'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as resources from 'vs/base/common/resources'; -import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation, testViewIcon } from 'vs/workbench/common/views'; -import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { coalesce, } from 'vs/base/common/arrays'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; -import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { Extensions as ViewletExtensions, ViewletRegistry } from 'vs/workbench/browser/viewlet'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Extensions as ViewContainerExtensions, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; +import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; import { VIEWLET_ID as NOTEBOOK } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet'; // {{SQL CARBON EDIT}} -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { URI } from 'vs/base/common/uri'; -import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -111,7 +106,7 @@ const viewDescriptor: IJSONSchema = { defaultSnippets: [{ body: { id: '${1:id}', name: '${2:name}' } }], properties: { type: { - markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), + markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), type: 'string', enum: [ 'tree', @@ -263,7 +258,8 @@ const viewsExtensionPoint: IExtensionPoint = ExtensionsR jsonSchema: viewsContribution }); -const TEST_VIEW_CONTAINER_ORDER = 6; +const CUSTOM_VIEWS_START_ORDER = 7; + class ViewsExtensionHandler implements IWorkbenchContribution { private viewContainersRegistry: IViewContainersRegistry; @@ -279,7 +275,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } private handleAndRegisterCustomViewContainers() { - this.registerTestViewContainer(); viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => { if (removed.length) { this.removeCustomViewContainers(removed); @@ -292,7 +287,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser[], existingViewContainers: ViewContainer[]): void { const viewContainersRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); - let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1; + let activityBarOrder = CUSTOM_VIEWS_START_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length; let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1; for (let { value, collector, description } of extensionPoints) { forEach(value, entry => { @@ -326,13 +321,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } } - private registerTestViewContainer(): void { - const title = localize('test', "Test"); - const icon = testViewIcon; - - this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar); - } - private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean { if (!Array.isArray(viewsContainersDescriptors)) { collector.error(localize('viewcontainer requirearray', "views containers must be an array")); @@ -392,7 +380,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { viewContainer = this.viewContainersRegistry.registerViewContainer({ id, - name: title, extensionId, + title, extensionId, ctorDescriptor: new SyncDescriptor( ViewPaneContainer, [id, { mergeViewWithContainerWhenSingleView: true }] @@ -402,23 +390,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { icon, }, location); - // Register Action to Open Viewlet - class OpenCustomViewletAction extends ShowViewletAction { - constructor( - id: string, label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, id, viewletService, editorGroupService, layoutService); - } - } - const registry = Registry.as(ActionExtensions.WorkbenchActions); - registry.registerWorkbenchAction( - SyncActionDescriptor.create(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)), - `View: Show ${title}`, - CATEGORIES.View.value - ); } return viewContainer; @@ -499,7 +470,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { name: item.name, when: ContextKeyExpr.deserialize(item.when), containerIcon: icon || viewContainer?.icon, - containerTitle: item.contextualTitle || viewContainer?.name, + containerTitle: item.contextualTitle || viewContainer?.title, canToggleVisibility: true, canMoveView: viewContainer?.id !== REMOTE, treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name) : undefined, @@ -509,7 +480,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { originalContainerId: entry.key, group: item.group, remoteAuthority: item.remoteName || (item).remoteAuthority, // TODO@roblou - delete after remote extensions are updated - hideByDefault: initialVisibility === InitialVisibility.Hidden + hideByDefault: initialVisibility === InitialVisibility.Hidden, + workspace: viewContainer?.id === REMOTE ? true : undefined }; diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 0cd18237ee..76702bea94 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -12,7 +12,6 @@ import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/works import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IViewDescriptorService, IViewsService, ViewVisibilityState } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -128,12 +127,6 @@ CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (access } }); -CommandsRegistry.registerCommand('_workbench.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { - // TODO: discuss martin, ben where to put this - const openerService = accessor.get(IOpenerService); - openerService.open(URI.revive(uri), options); -}); - CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (accessor: ServicesAccessor) { const logService = accessor.get(ILogService); diff --git a/src/vs/workbench/api/common/exHostSecretState.ts b/src/vs/workbench/api/common/exHostSecretState.ts new file mode 100644 index 0000000000..f8eac00bc4 --- /dev/null +++ b/src/vs/workbench/api/common/exHostSecretState.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from 'vs/workbench/api/common/extHost.protocol'; +import { Emitter } from 'vs/base/common/event'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export class ExtHostSecretState implements ExtHostSecretStateShape { + private _proxy: MainThreadSecretStateShape; + private _onDidChangePassword = new Emitter<{ extensionId: string, key: string }>(); + readonly onDidChangePassword = this._onDidChangePassword.event; + + constructor(mainContext: IExtHostRpcService) { + this._proxy = mainContext.getProxy(MainContext.MainThreadSecretState); + } + + async $onDidChangePassword(e: { extensionId: string, key: string }): Promise { + this._onDidChangePassword.fire(e); + } + + get(extensionId: string, key: string): Promise { + return this._proxy.$getPassword(extensionId, key); + } + + store(extensionId: string, key: string, value: string): Promise { + return this._proxy.$setPassword(extensionId, key, value); + } + + delete(extensionId: string, key: string): Promise { + return this._proxy.$deletePassword(extensionId, key); + } +} + +export interface IExtHostSecretState extends ExtHostSecretState { } +export const IExtHostSecretState = createDecorator('IExtHostSecretState'); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 07b89a94ef..a35fb52b6d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -82,6 +82,9 @@ import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPane import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting'; +import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener'; +import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -107,6 +110,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); + const extHostSecretState = accessor.get(IExtHostSecretState); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -117,6 +121,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService); rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); + rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -129,6 +134,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService)); // manually create and register addressable instances + const extHostEditorTabs = rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, new ExtHostEditorTabs()); const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); @@ -154,6 +160,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostDocumentsAndEditors, extHostWorkspace)); + const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); // Check that no named customers are missing // {{SQL CARBON EDIT}} filter out the services we don't expose @@ -212,22 +219,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeSessions(): Event { return extHostAuthentication.onDidChangeSessions; }, - registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { + registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostAuthentication.registerAuthenticationProvider(provider); + return extHostAuthentication.registerAuthenticationProvider(id, label, provider, options); }, get onDidChangeAuthenticationProviders(): Event { checkProposedApiEnabled(extension); return extHostAuthentication.onDidChangeAuthenticationProviders; }, - getProviderIds(): Thenable> { - checkProposedApiEnabled(extension); - return extHostAuthentication.getProviderIds(); - }, - get providerIds(): string[] { - checkProposedApiEnabled(extension); - return extHostAuthentication.providerIds; - }, get providers(): ReadonlyArray { checkProposedApiEnabled(extension); return extHostAuthentication.providers; @@ -235,22 +234,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I logout(providerId: string, sessionId: string): Thenable { checkProposedApiEnabled(extension); return extHostAuthentication.logout(providerId, sessionId); - }, - getPassword(key: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.getPassword(extension, key); - }, - setPassword(key: string, value: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.setPassword(extension, key, value); - }, - deletePassword(key: string): Thenable { - checkProposedApiEnabled(extension); - return extHostAuthentication.deletePassword(extension, key); - }, - get onDidChangePassword(): Event { - checkProposedApiEnabled(extension); - return extHostAuthentication.onDidChangePassword; } }; @@ -282,7 +265,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerDiffInformationCommand: (id: string, callback: (diff: vscode.LineChange[], ...args: any[]) => any, thisArg?: any): vscode.Disposable => { checkProposedApiEnabled(extension); return extHostCommands.registerCommand(true, id, async (...args: any[]): Promise => { - const activeTextEditor = extHostEditors.getActiveTextEditor(); + const activeTextEditor = extHostDocumentsAndEditors.activeEditor(true); if (!activeTextEditor) { extHostLogService.warn('Cannot execute ' + id + ' because there is no active text editor.'); return undefined; @@ -308,14 +291,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get appName() { return initData.environment.appName; }, get appRoot() { return initData.environment.appRoot?.fsPath ?? ''; }, get uriScheme() { return initData.environment.appUriScheme; }, - get clipboard(): vscode.Clipboard { - return extHostClipboard; - }, + get clipboard(): vscode.Clipboard { return extHostClipboard.value; }, get shell() { return extHostTerminalService.getDefaultShell(false, configProvider); }, - openExternal(uri: URI) { - return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.authority }); + openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string; }) { + return extHostWindow.openUri(uri, { + allowTunneling: !!initData.remote.authority, + allowContributedOpeners: options?.allowContributedOpeners, + }); }, asExternalUri(uri: URI) { if (uri.scheme === initData.environment.appUriScheme) { @@ -357,6 +341,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, + get onDidChangeTestResults() { + checkProposedApiEnabled(extension); + return extHostTesting.onLastResultsChanged; + }, + get testResults() { + checkProposedApiEnabled(extension); + return extHostTesting.lastResults; + }, }; // namespace: extensions @@ -483,6 +475,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getTokenInformationAtPosition(doc: vscode.TextDocument, pos: vscode.Position) { checkProposedApiEnabled(extension); return extHostLanguages.tokenAtPosition(doc, pos); + }, + registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider); } }; @@ -594,7 +590,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I priority = priority; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation, extension); + return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); @@ -694,6 +690,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showNotebookDocument(document, options?) { checkProposedApiEnabled(extension); return extHostNotebook.showNotebookDocument(document, options); + }, + registerExternalUriOpener(id: string, opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) { + checkProposedApiEnabled(extension); + return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata); + }, + get openEditors() { + checkProposedApiEnabled(extension); + return extHostEditorTabs.tabs; + }, + get onDidChangeOpenEditors() { + checkProposedApiEnabled(extension); + return extHostEditorTabs.onDidChangeTabs; } }; @@ -825,7 +833,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostFileSystem.registerFileSystemProvider(extension.identifier, scheme, provider, options); }, get fs() { - return extHostConsumerFileSystem; + return extHostConsumerFileSystem.value; }, registerFileSearchProvider: (scheme: string, provider: vscode.FileSearchProvider) => { checkProposedApiEnabled(extension); @@ -1125,6 +1133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall, + CancellationError: errors.CancellationError, CancellationTokenSource: CancellationTokenSource, CodeAction: extHostTypes.CodeAction, CodeActionKind: extHostTypes.CodeActionKind, @@ -1165,6 +1174,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, + ExternalUriOpenerPriority: extHostTypes.ExternalUriOpenerPriority, FileChangeType: extHostTypes.FileChangeType, FileDecoration: extHostTypes.FileDecoration, FileSystemError: extHostTypes.FileSystemError, @@ -1174,6 +1184,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FunctionBreakpoint: extHostTypes.FunctionBreakpoint, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, + InlineHint: extHostTypes.InlineHint, Location: extHostTypes.Location, MarkdownString: extHostTypes.MarkdownString, OverviewRulerLane: OverviewRulerLane, @@ -1224,71 +1235,71 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I WorkspaceEdit: extHostTypes.WorkspaceEdit, // proposed api types get RemoteAuthorityResolverError() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.RemoteAuthorityResolverError; }, get ResolvedAuthority() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.ResolvedAuthority; }, get SourceControlInputBoxValidationType() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.SourceControlInputBoxValidationType; }, get ExtensionRuntime() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.ExtensionRuntime; }, get TimelineItem() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.TimelineItem; }, get CellKind() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.CellKind; }, get CellOutputKind() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.CellOutputKind; }, get NotebookCellRunState() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.NotebookCellRunState; }, get NotebookRunState() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.NotebookRunState; }, get NotebookCellStatusBarAlignment() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.NotebookCellStatusBarAlignment; }, get NotebookEditorRevealType() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.NotebookEditorRevealType; }, get NotebookCellOutput() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.NotebookCellOutput; }, get NotebookCellOutputItem() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.NotebookCellOutputItem; }, get LinkedEditingRanges() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.LinkedEditingRanges; }, get TestRunState() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.TestRunState; }, get TestMessageSeverity() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.TestMessageSeverity; }, get TestState() { - checkProposedApiEnabled(extension); + // checkProposedApiEnabled(extension); return extHostTypes.TestState; }, }; @@ -1297,9 +1308,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I class Extension implements vscode.Extension { - private _extensionService: IExtHostExtensionService; - private _originExtensionId: ExtensionIdentifier; - private _identifier: ExtensionIdentifier; + #extensionService: IExtHostExtensionService; + #originExtensionId: ExtensionIdentifier; + #identifier: ExtensionIdentifier; readonly id: string; readonly extensionUri: URI; @@ -1308,9 +1319,9 @@ class Extension implements vscode.Extension { readonly extensionKind: vscode.ExtensionKind; constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: extHostTypes.ExtensionKind) { - this._extensionService = extensionService; - this._originExtensionId = originExtensionId; - this._identifier = description.identifier; + this.#extensionService = extensionService; + this.#originExtensionId = originExtensionId; + this.#identifier = description.identifier; this.id = description.identifier.value; this.extensionUri = description.extensionLocation; this.extensionPath = path.normalize(originalFSPath(description.extensionLocation)); @@ -1319,17 +1330,17 @@ class Extension implements vscode.Extension { } get isActive(): boolean { - return this._extensionService.isActivated(this._identifier); + return this.#extensionService.isActivated(this.#identifier); } get exports(): T { if (this.packageJSON.api === 'none') { return undefined!; // Strict nulloverride - Public api } - return this._extensionService.getExtensionExports(this._identifier); + return this.#extensionService.getExtensionExports(this.#identifier); } activate(): Thenable { - return this._extensionService.activateByIdWithErrors(this._identifier, { startup: false, extensionId: this._originExtensionId, activationEvent: 'api' }).then(() => this.exports); + return this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }).then(() => this.exports); } } diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index dbaacb6f54..73adf2b05f 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -21,6 +21,7 @@ import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow'; import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; +import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths); registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); @@ -39,3 +40,4 @@ registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); registerSingleton(IExtHostWindow, ExtHostWindow); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); +registerSingleton(IExtHostSecretState, ExtHostSecretState); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 75397c1d47..901cd5c5d4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IRemoteConsoleLog } from 'vs/base/common/console'; @@ -41,13 +42,13 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -57,7 +58,8 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; // {{SQL CARBON EDIT}} import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; @@ -112,7 +114,8 @@ export interface IConfigurationInitData extends IConfigurationData { } export interface IExtHostContext extends IRPCProtocol { - remoteAuthority: string | null; + readonly remoteAuthority: string | null; + readonly extensionHostKind: ExtensionHostKind; } export interface IMainContext extends IRPCProtocol { @@ -166,19 +169,12 @@ export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void; $unregisterAuthenticationProvider(id: string): void; $ensureProvider(id: string): Promise; - $getProviderIds(): Promise; $sendDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; - $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; - $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; - $loginPrompt(providerName: string, extensionName: string): Promise; - $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise; - $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; - - $getSessions(providerId: string): Promise>; - $login(providerId: string, scopes: string[]): Promise; $logout(providerId: string, sessionId: string): Promise; +} +export interface MainThreadSecretStateShape extends IDisposable { $getPassword(extensionId: string, key: string): Promise; $setPassword(extensionId: string, key: string, value: string): Promise; $deletePassword(extensionId: string, key: string): Promise; @@ -236,7 +232,6 @@ export interface MainThreadDocumentsShape extends IDisposable { export interface ITextEditorConfigurationUpdate { tabSize?: number | 'auto'; - indentSize?: number | 'tabSize'; insertSpaces?: boolean | 'auto'; cursorStyle?: TextEditorCursorStyle; lineNumbers?: RenderLineNumbersType; @@ -244,7 +239,6 @@ export interface ITextEditorConfigurationUpdate { export interface IResolvedTextEditorConfiguration { tabSize: number; - indentSize: number; insertSpaces: boolean; cursorStyle: TextEditorCursorStyle; lineNumbers: RenderLineNumbersType; @@ -334,7 +328,7 @@ export interface IIndentationRuleDto { export interface IOnEnterRuleDto { beforeText: IRegExpDto; afterText?: IRegExpDto; - oneLineAboveText?: IRegExpDto; + previousLineText?: IRegExpDto; action: EnterAction; } export interface ILanguageConfigurationDto { @@ -401,6 +395,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitInlineHintsEvent(eventHandle: number, event?: any): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; @@ -419,6 +415,7 @@ export interface MainThreadLanguagesShape extends IDisposable { export interface MainThreadMessageOptions { extension?: IExtensionDescription; modal?: boolean; + useCustom?: boolean; } export interface MainThreadMessageServiceShape extends IDisposable { @@ -442,6 +439,16 @@ export interface MainThreadProgressShape extends IDisposable { $progressEnd(handle: number): void; } +/** + * A terminal that is created on the extension host side is temporarily assigned + * a UUID by the extension host that created it. Once the renderer side has assigned + * a real numeric id, the numeric id will be used. + * + * All other terminals (that are not created on the extension host side) always + * use the numeric id. + */ +export type TerminalIdentifier = number | string; + export interface TerminalLaunchConfig { name?: string; shellPath?: string; @@ -456,11 +463,11 @@ export interface TerminalLaunchConfig { } export interface MainThreadTerminalServiceShape extends IDisposable { - $createTerminal(config: TerminalLaunchConfig): Promise<{ id: number, name: string; }>; - $dispose(terminalId: number): void; - $hide(terminalId: number): void; - $sendText(terminalId: number, text: string, addNewLine: boolean): void; - $show(terminalId: number, preserveFocus: boolean): void; + $createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise; + $dispose(id: TerminalIdentifier): void; + $hide(id: TerminalIdentifier): void; + $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void; + $show(id: TerminalIdentifier, preserveFocus: boolean): void; $startSendingDataEvents(): void; $stopSendingDataEvents(): void; $startLinkProvider(): void; @@ -598,6 +605,24 @@ export interface ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void; } +//#region --- open editors model + +export interface MainThreadEditorTabsShape extends IDisposable { + // manage tabs: move, close, rearrange etc +} + +export interface IEditorTabDto { + group: number; + name: string; + resource: UriComponents +} + +export interface IExtHostEditorTabsShape { + $acceptEditorTabs(tabs: IEditorTabDto[]): void; +} + +//#endregion + export type WebviewHandle = string; export interface WebviewPanelShowOptions { @@ -743,6 +768,7 @@ export enum NotebookEditorRevealType { Default = 0, InCenter = 1, InCenterIfOutsideViewport = 2, + AtTop = 3 } export interface INotebookDocumentShowOptions { @@ -776,8 +802,6 @@ export interface MainThreadNotebookShape extends IDisposable { $registerNotebookEditorDecorationType(key: string, options: INotebookDecorationRenderOptions): void; $removeNotebookEditorDecorationType(key: string): void; $trySetDecorations(id: string, range: ICellRange, decorationKey: string): void; - $onUndoableContentChange(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; - $onContentChange(resource: UriComponents, viewType: string): void; } export interface MainThreadUrlsShape extends IDisposable { @@ -790,6 +814,16 @@ export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; } +export interface MainThreadUriOpenersShape extends IDisposable { + $registerUriOpener(id: string, schemes: readonly string[], extensionId: ExtensionIdentifier, label: string): Promise; + $unregisterUriOpener(id: string): Promise; +} + +export interface ExtHostUriOpenersShape { + $canOpenUri(id: string, uri: UriComponents, token: CancellationToken): Promise; + $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise; +} + export interface ITextSearchComplete { limitHit?: boolean; } @@ -857,6 +891,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; $onExtensionHostExit(code: number): Promise; + $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; } export interface SCMProviderFeatures { @@ -950,6 +985,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { export interface IOpenUriOptions { readonly allowTunneling?: boolean; + readonly allowContributedOpeners?: boolean | string; } export interface MainThreadWindowShape extends IDisposable { @@ -962,8 +998,9 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions, source: string | undefined): Promise; $closeTunnel(remote: { host: string, port: number }): Promise; $getTunnels(): Promise; - $setTunnelProvider(): Promise; - $tunnelServiceReady(): Promise; + $setTunnelProvider(features: TunnelProviderFeatures): Promise; + $setCandidateFinder(): Promise; + $setCandidateFilter(): Promise; $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise; } @@ -1057,7 +1094,7 @@ export interface ExtHostTreeViewsShape { $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; $hasResolve(treeViewId: string): Promise; - $resolve(treeViewId: string, treeItemHandle: string): Promise; + $resolve(treeViewId: string, treeItemHandle: string, token: CancellationToken): Promise; } export interface ExtHostWorkspaceShape { @@ -1099,7 +1136,10 @@ export interface ExtHostAuthenticationShape { $onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise; $onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]): Promise; $setProviders(providers: modes.AuthenticationProviderInformation[]): Promise; - $onDidChangePassword(): Promise; +} + +export interface ExtHostSecretStateShape { + $onDidChangePassword(e: { extensionId: string, key: string }): Promise; } export interface ExtHostSearchShape { @@ -1150,9 +1190,14 @@ export interface SourceTargetPair { target: UriComponents; } +export interface IWillRunFileOperationParticipation { + edit: IWorkspaceEditDto; + extensionNames: string[] +} + export interface ExtHostFileSystemEventServiceShape { $onFileEvent(events: FileSystemEvents): void; - $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise; + $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise; $onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void; } @@ -1257,6 +1302,18 @@ export interface ISignatureHelpContextDto { readonly activeSignatureHelp?: ISignatureHelpDto; } +export interface IInlineHintDto { + text: string; + range: IRange; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface IInlineHintsDto { + hints: IInlineHintDto[] +} + export interface ILocationDto { uri: UriComponents; range: IRange; @@ -1446,6 +1503,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveDocumentLink(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseDocumentLinks(handle: number, id: number): void; @@ -1478,6 +1536,7 @@ export interface IShellLaunchConfigDto { cwd?: string | UriComponents; env?: { [key: string]: string | null; }; hideFromUser?: boolean; + flowControl?: boolean; } export interface IShellDefinitionDto { @@ -1508,7 +1567,7 @@ export interface ITerminalDimensionsDto { export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number, exitCode: number | undefined): void; - $acceptTerminalOpened(id: number, name: string, shellLaunchConfig: IShellLaunchConfigDto): void; + $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfig: IShellLaunchConfigDto): void; $acceptActiveTerminalChanged(id: number | null): void; $acceptTerminalProcessId(id: number, processId: number): void; $acceptTerminalProcessData(id: number, data: string): void; @@ -1517,6 +1576,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $spawnExtHostProcess(id: number, shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; + $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; $acceptProcessShutdown(id: number, immediate: boolean): void; @@ -1613,7 +1673,7 @@ export type IDebugSessionDto = IDebugSessionFullDto | DebugSessionUUID; export interface ExtHostDebugServiceShape { $substituteVariables(folder: UriComponents | undefined, config: IConfig): Promise; - $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise; + $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise; $startDASession(handle: number, session: IDebugSessionDto): Promise; $stopDASession(handle: number): Promise; $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; @@ -1730,16 +1790,13 @@ export interface ExtHostNotebookShape { $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; - $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; + $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; $acceptModelSaved(uriComponents: UriComponents): void; $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; - $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; - $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; - } export interface ExtHostStorageShape { @@ -1754,9 +1811,11 @@ export interface MainThreadThemingShape extends IDisposable { } export interface ExtHostTunnelServiceShape { - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; + $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise; $closeTunnel(remote: { host: string, port: number }, silent?: boolean): Promise; $onDidTunnelsChange(): Promise; + $registerCandidateFinder(enable: boolean): Promise; + $applyCandidateFilter(candidates: CandidatePort[]): Promise; } export interface ExtHostTimelineShape { @@ -1769,11 +1828,12 @@ export const enum ExtHostTestingResource { } export interface ExtHostTestingShape { - $runTestsForProvider(req: RunTestForProviderRequest): Promise; + $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise; $subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void; - + $lookupTest(test: TestIdWithProvider): Promise; $acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; + $publishTestResults(results: InternalTestResults): void; } export interface MainThreadTestingShape { @@ -1782,7 +1842,7 @@ export interface MainThreadTestingShape { $subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; - $runTests(req: RunTestsRequest): Promise; + $runTests(req: RunTestsRequest, token: CancellationToken): Promise; } // --- proxy identifiers @@ -1803,6 +1863,7 @@ export const MainContext = { MainThreadDocumentContentProviders: createMainId('MainThreadDocumentContentProviders'), MainThreadTextEditors: createMainId('MainThreadTextEditors'), MainThreadEditorInsets: createMainId('MainThreadEditorInsets'), + MainThreadEditorTabs: createMainId('MainThreadEditorTabs'), MainThreadErrors: createMainId('MainThreadErrors'), MainThreadTreeViews: createMainId('MainThreadTreeViews'), MainThreadDownloadService: createMainId('MainThreadDownloadService'), @@ -1815,6 +1876,7 @@ export const MainContext = { MainThreadProgress: createMainId('MainThreadProgress'), MainThreadQuickOpen: createMainId('MainThreadQuickOpen'), MainThreadStatusBar: createMainId('MainThreadStatusBar'), + MainThreadSecretState: createMainId('MainThreadSecretState'), MainThreadStorage: createMainId('MainThreadStorage'), MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), @@ -1823,6 +1885,7 @@ export const MainContext = { MainThreadWebviewViews: createMainId('MainThreadWebviewViews'), MainThreadCustomEditors: createMainId('MainThreadCustomEditors'), MainThreadUrls: createMainId('MainThreadUrls'), + MainThreadUriOpeners: createMainId('MainThreadUriOpeners'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), @@ -1868,10 +1931,13 @@ export const ExtHostContext = { ExtHostCustomEditors: createExtId('ExtHostCustomEditors'), ExtHostWebviewViews: createExtId('ExtHostWebviewViews'), ExtHostEditorInsets: createExtId('ExtHostEditorInsets'), + ExtHostEditorTabs: createExtId('ExtHostEditorTabs'), ExtHostProgress: createMainId('ExtHostProgress'), ExtHostComments: createMainId('ExtHostComments'), + ExtHostSecretState: createMainId('ExtHostSecretState'), ExtHostStorage: createMainId('ExtHostStorage'), ExtHostUrls: createExtId('ExtHostUrls'), + ExtHostUriOpeners: createExtId('ExtHostUriOpeners'), ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 77e0a7ea4a..390ba232ec 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -20,6 +20,8 @@ import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; import { TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; //#region --- NEW world @@ -176,6 +178,57 @@ const newCommands: ApiCommand[] = [ [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('linkResolveCount', 'Number of links that should be resolved, only when links are unresolved.').optional()], new ApiCommandResult('A promise that resolves to an array of DocumentLink-instances.', value => value.map(typeConverters.DocumentLink.to)) ), + // --- semantic tokens + new ApiCommand( + 'vscode.provideDocumentSemanticTokensLegend', '_provideDocumentSemanticTokensLegend', 'Provide semantic tokens legend for a document', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { + if (!value) { + return undefined; + } + return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); + }) + ), + new ApiCommand( + 'vscode.provideDocumentSemanticTokens', '_provideDocumentSemanticTokens', 'Provide semantic tokens for a document', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { + if (!value) { + return undefined; + } + const semanticTokensDto = decodeSemanticTokensDto(value); + if (semanticTokensDto.type !== 'full') { + // only accepting full semantic tokens from provideDocumentSemanticTokens + return undefined; + } + return new types.SemanticTokens(semanticTokensDto.data, undefined); + }) + ), + new ApiCommand( + 'vscode.provideDocumentRangeSemanticTokensLegend', '_provideDocumentRangeSemanticTokensLegend', 'Provide semantic tokens legend for a document range', + [ApiCommandArgument.Uri], + new ApiCommandResult('A promise that resolves to SemanticTokensLegend.', value => { + if (!value) { + return undefined; + } + return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers); + }) + ), + new ApiCommand( + 'vscode.provideDocumentRangeSemanticTokens', '_provideDocumentRangeSemanticTokens', 'Provide semantic tokens for a document range', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to SemanticTokens.', value => { + if (!value) { + return undefined; + } + const semanticTokensDto = decodeSemanticTokensDto(value); + if (semanticTokensDto.type !== 'full') { + // only accepting full semantic tokens from provideDocumentRangeSemanticTokens + return undefined; + } + return new types.SemanticTokens(semanticTokensDto.data, undefined); + }) + ), // --- completions new ApiCommand( 'vscode.executeCompletionItemProvider', '_executeCompletionItemProvider', 'Execute completion item provider.', @@ -271,13 +324,21 @@ const newCommands: ApiCommand[] = [ return []; }) ), + // --- inline hints + new ApiCommand( + 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to an array of InlineHint objects', result => { + return result.map(typeConverters.InlineHint.to); + }) + ), // --- notebooks new ApiCommand( 'vscode.resolveNotebookContentProviders', '_resolveNotebookContentProvider', 'Resolve Notebook Content Providers', [ - new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), - new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), - new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), + // new ApiCommandArgument('viewType', '', v => typeof v === 'string', v => v), + // new ApiCommandArgument('displayName', '', v => typeof v === 'string', v => v), + // new ApiCommandArgument('options', '', v => typeof v === 'object', v => v), ], new ApiCommandResult<{ viewType: string; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index e36faf5e8a..3832702653 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -15,11 +15,15 @@ interface GetSessionsRequest { result: Promise; } +interface ProviderWithMetadata { + label: string; + provider: vscode.AuthenticationProvider; + options: vscode.AuthenticationProviderOptions; +} + export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _proxy: MainThreadAuthenticationShape; - private _authenticationProviders: Map = new Map(); - - private _providerIds: string[] = []; + private _authenticationProviders: Map = new Map(); private _providers: vscode.AuthenticationProviderInformation[] = []; @@ -29,9 +33,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeSessions = new Emitter(); readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; - private _onDidChangePassword = new Emitter(); - readonly onDidChangePassword: Event = this._onDidChangePassword.event; - private _inFlightRequests = new Map(); constructor(mainContext: IMainContext) { @@ -43,14 +44,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return Promise.resolve(); } - getProviderIds(): Promise> { - return this._proxy.$getProviderIds(); - } - - get providerIds(): string[] { - return this._providerIds; - } - get providers(): ReadonlyArray { return Object.freeze(this._providers.slice()); } @@ -90,128 +83,83 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { await this._proxy.$ensureProvider(providerId); - const provider = this._authenticationProviders.get(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; - - if (!provider) { - return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); - } - - const orderedScopes = scopes.sort().join(' '); - const sessions = (await provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); - - let session: vscode.AuthenticationSession | undefined = undefined; - if (sessions.length) { - if (!provider.supportsMultipleAccounts) { - session = sessions[0]; - const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, provider.label, extensionId, extensionName); - if (!allowed) { - throw new Error('User did not consent to login.'); - } - } else { - // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this._proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); - session = sessions.find(session => session.id === selected.id); - } - - } else { - if (options.createIfNone) { - const isAllowed = await this._proxy.$loginPrompt(provider.label, extensionName); - if (!isAllowed) { - throw new Error('User did not consent to login.'); - } - - session = await provider.login(scopes); - await this._proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); - } else { - await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); - } - } - - - return session; + return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); } async logout(providerId: string, sessionId: string): Promise { - const provider = this._authenticationProviders.get(providerId); - if (!provider) { + const providerData = this._authenticationProviders.get(providerId); + if (!providerData) { return this._proxy.$logout(providerId, sessionId); } - return provider.logout(sessionId); + return providerData.provider.logout(sessionId); } - registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { - if (this._authenticationProviders.get(provider.id)) { - throw new Error(`An authentication provider with id '${provider.id}' is already registered.`); + registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable { + if (this._authenticationProviders.get(id)) { + throw new Error(`An authentication provider with id '${id}' is already registered.`); } - this._authenticationProviders.set(provider.id, provider); - if (!this._providerIds.includes(provider.id)) { - this._providerIds.push(provider.id); - } + this._authenticationProviders.set(id, { label, provider, options: options ?? { supportsMultipleAccounts: false } }); - if (!this._providers.find(p => p.id === provider.id)) { + if (!this._providers.find(p => p.id === id)) { this._providers.push({ - id: provider.id, - label: provider.label + id: id, + label: label }); } const listener = provider.onDidChangeSessions(e => { - this._proxy.$sendDidChangeSessions(provider.id, e); + this._proxy.$sendDidChangeSessions(id, e); }); - this._proxy.$registerAuthenticationProvider(provider.id, provider.label, provider.supportsMultipleAccounts); + this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false); return new Disposable(() => { listener.dispose(); - this._authenticationProviders.delete(provider.id); - const index = this._providerIds.findIndex(id => id === provider.id); - if (index > -1) { - this._providerIds.splice(index); - } + this._authenticationProviders.delete(id); - const i = this._providers.findIndex(p => p.id === provider.id); + const i = this._providers.findIndex(p => p.id === id); if (i > -1) { this._providers.splice(i); } - this._proxy.$unregisterAuthenticationProvider(provider.id); + this._proxy.$unregisterAuthenticationProvider(id); }); } $login(providerId: string, scopes: string[]): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.login(scopes)); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.login(scopes)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } $logout(providerId: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.logout(sessionId)); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.logout(sessionId)); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } $getSessions(providerId: string): Promise> { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - return Promise.resolve(authProvider.getSessions()); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + return Promise.resolve(providerData.provider.getSessions()); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } async $getSessionAccessToken(providerId: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(providerId); - if (authProvider) { - const sessions = await authProvider.getSessions(); + const providerData = this._authenticationProviders.get(providerId); + if (providerData) { + const sessions = await providerData.provider.getSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { return session.accessToken; @@ -245,23 +193,4 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._onDidChangeAuthenticationProviders.fire({ added, removed }); return Promise.resolve(); } - - async $onDidChangePassword(): Promise { - this._onDidChangePassword.fire(); - } - - getPassword(requestingExtension: IExtensionDescription, key: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$getPassword(extensionId, key); - } - - setPassword(requestingExtension: IExtensionDescription, key: string, value: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$setPassword(extensionId, key, value); - } - - deletePassword(requestingExtension: IExtensionDescription, key: string): Promise { - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - return this._proxy.$deletePassword(extensionId, key); - } } diff --git a/src/vs/workbench/api/common/extHostClipboard.ts b/src/vs/workbench/api/common/extHostClipboard.ts index 49f9423270..7ef1fbcfdd 100644 --- a/src/vs/workbench/api/common/extHostClipboard.ts +++ b/src/vs/workbench/api/common/extHostClipboard.ts @@ -3,22 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMainContext, MainContext, MainThreadClipboardShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import type * as vscode from 'vscode'; -export class ExtHostClipboard implements vscode.Clipboard { +export class ExtHostClipboard { - private readonly _proxy: MainThreadClipboardShape; + readonly value: vscode.Clipboard; constructor(mainContext: IMainContext) { - this._proxy = mainContext.getProxy(MainContext.MainThreadClipboard); - } - - readText(): Promise { - return this._proxy.$readText(); - } - - writeText(value: string): Promise { - return this._proxy.$writeText(value); + const proxy = mainContext.getProxy(MainContext.MainThreadClipboard); + this.value = Object.freeze({ + readText() { + return proxy.$readText(); + }, + writeText(value: string) { + return proxy.$writeText(value); + } + }); } } diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index ac8c402c59..29c9c4e6fc 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -44,8 +44,8 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { createWebviewEditorInset(editor: vscode.TextEditor, line: number, height: number, options: vscode.WebviewOptions | undefined, extension: IExtensionDescription): vscode.WebviewEditorInset { let apiEditor: ExtHostTextEditor | undefined; - for (const candidate of this._editors.getVisibleTextEditors()) { - if (candidate === editor) { + for (const candidate of this._editors.getVisibleTextEditors(true)) { + if (candidate.value === editor) { apiEditor = candidate; break; } @@ -121,7 +121,7 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { } }; - this._proxy.$createEditorInset(handle, apiEditor.id, apiEditor.document.uri, line + 1, height, options || {}, extension.identifier, extension.extensionLocation); + this._proxy.$createEditorInset(handle, apiEditor.id, apiEditor.value.document.uri, line + 1, height, options || {}, extension.identifier, extension.extensionLocation); this._insets.set(handle, { editor, inset, onDidReceiveMessage }); return inset; diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 5166827f4d..12c75e0476 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -103,7 +103,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { const internalArgs = apiCommand.args.map((arg, i) => { if (!arg.validate(apiArgs[i])) { - throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', receieved: ${apiArgs[i]}`); + throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', received: ${apiArgs[i]}`); } return arg.convert(apiArgs[i]); }); @@ -194,7 +194,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { } } - private _executeContributedCommand(id: string, args: any[]): Promise { + private async _executeContributedCommand(id: string, args: any[]): Promise { const command = this._commands.get(id); if (!command) { throw new Error('Unknown command'); @@ -205,17 +205,16 @@ export class ExtHostCommands implements ExtHostCommandsShape { try { validateConstraint(args[i], description.args[i].constraint); } catch (err) { - return Promise.reject(new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`)); + throw new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`); } } } try { - const result = callback.apply(thisArg, args); - return Promise.resolve(result); + return await callback.apply(thisArg, args); } catch (err) { this._logService.error(err, id); - return Promise.reject(new Error(`Running the contributed command: '${id}' failed.`)); + throw new Error(`Running the contributed command: '${id}' failed.`); } } diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index cfaaef88ad..4d0a9ea6ba 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -13,6 +13,7 @@ import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; @@ -178,7 +179,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, ): vscode.Disposable { const disposables = new DisposableStore(); - if ('resolveCustomTextEditor' in provider) { + if (isCustomTextEditorProvider(provider)) { disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { supportsMove: !!provider.moveCustomTextEditor, @@ -208,7 +209,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor })); } - async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { const entry = this._editorProviders.get(viewType); if (!entry) { @@ -261,8 +261,10 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor throw new Error(`No provider found for '${viewType}'`); } + const viewColumn = typeConverters.ViewColumn.to(position); + const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); - const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); const revivedResource = URI.revive(resource); @@ -350,7 +352,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor return backup.id; } - private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { const entry = this._documents.get(viewType, URI.revive(resource)); if (!entry) { @@ -375,6 +376,9 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor } } +function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider { + return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function'; +} function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 69f69926a6..de7a6dab4f 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -89,7 +89,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E get onDidReceiveDebugSessionCustomEvent(): Event { return this._onDidReceiveDebugSessionCustomEvent.event; } private _activeDebugConsole: ExtHostDebugConsole; - get activeDebugConsole(): ExtHostDebugConsole { return this._activeDebugConsole; } + get activeDebugConsole(): vscode.DebugConsole { return this._activeDebugConsole.value; } private _breakpoints: Map; private _breakpointEventsActive: boolean; @@ -152,7 +152,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E const source = src; - if (typeof source.sourceReference === 'number') { + if (typeof source.sourceReference === 'number' && source.sourceReference > 0) { // src can be retrieved via DAP's "source" request let debug = `debug:${encodeURIComponent(source.path || '')}`; @@ -361,7 +361,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E // RPC methods (ExtHostDebugServiceShape) - public async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise { + public async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise { return Promise.resolve(undefined); } @@ -911,20 +911,20 @@ export class ExtHostDebugSession implements vscode.DebugSession { } } -export class ExtHostDebugConsole implements vscode.DebugConsole { +export class ExtHostDebugConsole { - private _debugServiceProxy: MainThreadDebugServiceShape; + readonly value: vscode.DebugConsole; constructor(proxy: MainThreadDebugServiceShape) { - this._debugServiceProxy = proxy; - } - append(value: string): void { - this._debugServiceProxy.$appendDebugConsole(value); - } - - appendLine(value: string): void { - this.append(value + '\n'); + this.value = Object.freeze({ + append(value: string): void { + proxy.$appendDebugConsole(value); + }, + appendLine(value: string): void { + this.append(value + '\n'); + } + }); } } diff --git a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts index 265c350aae..3190a74949 100644 --- a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts @@ -18,6 +18,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; +import { Lazy } from 'vs/base/common/lazy'; class Reference { private _count = 0; @@ -49,13 +50,13 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha private readonly _onDidAddDocuments = new Emitter(); private readonly _onDidRemoveDocuments = new Emitter(); - private readonly _onDidChangeVisibleTextEditors = new Emitter(); - private readonly _onDidChangeActiveTextEditor = new Emitter(); + private readonly _onDidChangeVisibleTextEditors = new Emitter(); + private readonly _onDidChangeActiveTextEditor = new Emitter(); readonly onDidAddDocuments: Event = this._onDidAddDocuments.event; readonly onDidRemoveDocuments: Event = this._onDidRemoveDocuments.event; - readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; - readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; + readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; + readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; constructor( @IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService, @@ -135,7 +136,7 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha data.id, this._extHostRpc.getProxy(MainContext.MainThreadTextEditors), this._logService, - documentData, + new Lazy(() => documentData.document), data.selections.map(typeConverters.Selection.to), data.options, data.visibleRanges.map(range => typeConverters.Range.to(range)), @@ -162,7 +163,7 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha } if (delta.removedEditors || delta.addedEditors) { - this._onDidChangeVisibleTextEditors.fire(this.allEditors()); + this._onDidChangeVisibleTextEditors.fire(this.allEditors().map(editor => editor.value)); } if (delta.newActiveEditor !== undefined) { this._onDidChangeActiveTextEditor.fire(this.activeEditor()); @@ -181,11 +182,17 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha return this._editors.get(id); } - activeEditor(): ExtHostTextEditor | undefined { + activeEditor(): vscode.TextEditor | undefined; + activeEditor(internal: true): ExtHostTextEditor | undefined; + activeEditor(internal?: true): vscode.TextEditor | ExtHostTextEditor | undefined { if (!this._activeEditorId) { return undefined; + } + const editor = this._editors.get(this._activeEditorId); + if (internal) { + return editor; } else { - return this._editors.get(this._activeEditorId); + return editor?.value; } } diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts new file mode 100644 index 0000000000..cdb75e8034 --- /dev/null +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { IEditorTabDto, IExtHostEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; + + +export interface IEditorTab { + name: string; + group: number; + resource: vscode.Uri +} + +export class ExtHostEditorTabs implements IExtHostEditorTabsShape { + + private readonly _onDidChangeTabs = new Emitter(); + readonly onDidChangeTabs: Event = this._onDidChangeTabs.event; + + private _tabs: IEditorTab[] = []; + + get tabs(): readonly IEditorTab[] { + return this._tabs; + } + + $acceptEditorTabs(tabs: IEditorTabDto[]): void { + this._tabs = tabs.map(dto => { + return { + name: dto.name, + group: dto.group, + resource: URI.revive(dto.resource) + }; + }); + this._onDidChangeTabs.fire(); + } +} diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 4c51767140..20ca789c3a 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,6 +5,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; +import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -35,6 +36,8 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, Event } from 'vs/base/common/event'; import { IExtensionActivationHost, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; +import { ExtHostSecretState, IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtensionSecrets } from 'vs/workbench/api/common/extHostSecrets'; interface ITestRunner { /** Old test runner API, as exported from `vscode/lib/testrunner` */ @@ -50,7 +53,7 @@ export const IHostUtils = createDecorator('IHostUtils'); export interface IHostUtils { readonly _serviceBrand: undefined; - exit(code?: number): void; + exit(code: number): void; exists(path: string): Promise; realpath(path: string): Promise; } @@ -94,6 +97,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _readyToRunExtensions: Barrier; protected readonly _registry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; + private readonly _secretState: ExtHostSecretState; private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; private _extensionPathIndex: Promise> | null; @@ -115,7 +119,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme @IExtHostInitDataService initData: IExtHostInitDataService, @IExtensionStoragePaths storagePath: IExtensionStoragePaths, @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService, - @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService + @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, ) { super(); this._hostUtils = hostUtils; @@ -138,10 +142,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToRunExtensions = new Barrier(); this._registry = new ExtensionDescriptionRegistry(this._initData.extensions); this._storage = new ExtHostStorage(this._extHostContext); + this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; this._instaService = instaService.createChild(new ServiceCollection( - [IExtHostStorage, this._storage] + [IExtHostStorage, this._storage], + [IExtHostSecretState, this._secretState] )); const hostExtensions = new Set(); @@ -184,6 +190,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._almostReadyToRunExtensions.open(); await this._extHostWorkspace.waitForInitializeCall(); + performance.mark('code/extHost/ready'); this._readyToStartExtensionHost.open(); if (this._initData.autoStart) { @@ -362,10 +369,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), + this._loadCommonJSModule(extensionDescription.identifier, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), this._loadExtensionContext(extensionDescription) ]).then(values => { + performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`); return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); + }).then((activatedExtension) => { + performance.mark(`code/extHost/didActivateExtension/${extensionDescription.identifier.value}`); + return activatedExtension; }); } @@ -373,6 +384,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage); const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); + const secrets = new ExtensionSecrets(extensionDescription, this._secretState); const extensionMode = extensionDescription.isUnderDevelopment ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) : ExtensionMode.Production; @@ -388,6 +400,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return Object.freeze({ globalState, workspaceState, + secrets, subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, @@ -456,6 +469,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } private _activateAllStartupFinished(): void { + // startup is considered finished + this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks()); + for (const desc of this._registry.getAllExtensionDescriptions()) { if (desc.activationEvents) { for (const activationEvent of desc.activationEvents) { @@ -541,7 +557,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme let testRunner: ITestRunner | INewTestRunner | undefined; let requireError: Error | undefined; try { - testRunner = await this._loadCommonJSModule(URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); + testRunner = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); } catch (error) { requireError = error; } @@ -586,11 +602,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } private _testRunnerExit(code: number): void { + this._logService.info(`extension host terminating: test runner requested exit with code ${code}`); + this._logService.flush(); + // wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain // (this is to ensure all outstanding messages reach the renderer) const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code); const drainPromise = this._extHostContext.drain(); Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => { + this._logService.info(`exiting with code ${code}`); + this._logService.flush(); + this._hostUtils.exit(code); }); } @@ -644,7 +666,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } try { + performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`); const result = await resolver.resolve(remoteAuthority, { resolveAttempt }); + performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`); this._disposables.add(await this._extHostTunnelService.setTunnelExtensionFunctions(resolver)); // Split merged API result into separate authority/options @@ -667,6 +691,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } }; } catch (err) { + performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`); if (err instanceof RemoteAuthorityResolverError) { return { type: 'error', @@ -754,7 +779,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/common/extHostFileSystemConsumer.ts b/src/vs/workbench/api/common/extHostFileSystemConsumer.ts index 06d27b1797..9c9b832336 100644 --- a/src/vs/workbench/api/common/extHostFileSystemConsumer.ts +++ b/src/vs/workbench/api/common/extHostFileSystemConsumer.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainThreadFileSystemShape, MainContext } from './extHost.protocol'; +import { MainContext } from './extHost.protocol'; import * as vscode from 'vscode'; import * as files from 'vs/platform/files/common/files'; import { FileSystemError } from 'vs/workbench/api/common/extHostTypes'; @@ -12,49 +12,51 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; -export class ExtHostConsumerFileSystem implements vscode.FileSystem { +export class ExtHostConsumerFileSystem { readonly _serviceBrand: undefined; - private readonly _proxy: MainThreadFileSystemShape; + readonly value: vscode.FileSystem; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostFileSystemInfo private readonly _fileSystemInfo: IExtHostFileSystemInfo, + @IExtHostFileSystemInfo fileSystemInfo: IExtHostFileSystemInfo, ) { - this._proxy = extHostRpc.getProxy(MainContext.MainThreadFileSystem); - } + const proxy = extHostRpc.getProxy(MainContext.MainThreadFileSystem); - stat(uri: vscode.Uri): Promise { - return this._proxy.$stat(uri).catch(ExtHostConsumerFileSystem._handleError); - } - readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { - return this._proxy.$readdir(uri).catch(ExtHostConsumerFileSystem._handleError); - } - createDirectory(uri: vscode.Uri): Promise { - return this._proxy.$mkdir(uri).catch(ExtHostConsumerFileSystem._handleError); - } - async readFile(uri: vscode.Uri): Promise { - return this._proxy.$readFile(uri).then(buff => buff.buffer).catch(ExtHostConsumerFileSystem._handleError); - } - writeFile(uri: vscode.Uri, content: Uint8Array): Promise { - return this._proxy.$writeFile(uri, VSBuffer.wrap(content)).catch(ExtHostConsumerFileSystem._handleError); - } - delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean; }): Promise { - return this._proxy.$delete(uri, { ...{ recursive: false, useTrash: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError); - } - rename(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; }): Promise { - return this._proxy.$rename(oldUri, newUri, { ...{ overwrite: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError); - } - copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean; }): Promise { - return this._proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError); - } - isWritableFileSystem(scheme: string): boolean | undefined { - const capabilities = this._fileSystemInfo.getCapabilities(scheme); - if (typeof capabilities === 'number') { - return !(capabilities & files.FileSystemProviderCapabilities.Readonly); - } - return undefined; + this.value = Object.freeze({ + stat(uri: vscode.Uri): Promise { + return proxy.$stat(uri).catch(ExtHostConsumerFileSystem._handleError); + }, + readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + return proxy.$readdir(uri).catch(ExtHostConsumerFileSystem._handleError); + }, + createDirectory(uri: vscode.Uri): Promise { + return proxy.$mkdir(uri).catch(ExtHostConsumerFileSystem._handleError); + }, + async readFile(uri: vscode.Uri): Promise { + return proxy.$readFile(uri).then(buff => buff.buffer).catch(ExtHostConsumerFileSystem._handleError); + }, + writeFile(uri: vscode.Uri, content: Uint8Array): Promise { + return proxy.$writeFile(uri, VSBuffer.wrap(content)).catch(ExtHostConsumerFileSystem._handleError); + }, + delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean; }): Promise { + return proxy.$delete(uri, { ...{ recursive: false, useTrash: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError); + }, + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; }): Promise { + return proxy.$rename(oldUri, newUri, { ...{ overwrite: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError); + }, + copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean; }): Promise { + return proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError); + }, + isWritableFileSystem(scheme: string): boolean | undefined { + const capabilities = fileSystemInfo.getCapabilities(scheme); + if (typeof capabilities === 'number') { + return !(capabilities & files.FileSystemProviderCapabilities.Readonly); + } + return undefined; + } + }); } private static _handleError(err: any): never { diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 5f3ee8a299..9b6dba0f04 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -3,12 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AsyncEmitter, Emitter, Event, IWaitUntil } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; +import { AsyncEmitter, IWaitUntil } from 'vs/base/common/async'; import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, SourceTargetPair, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -122,8 +123,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ constructor( mainContext: IMainContext, private readonly _logService: ILogService, - private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape = mainContext.getProxy(MainContext.MainThreadBulkEdits) + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors ) { // } @@ -178,24 +178,21 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ }; } - async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise { + async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise { switch (operation) { case FileOperation.MOVE: - await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, undoRedoGroupId, timeout, token); - break; + return await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token); case FileOperation.DELETE: - await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token); - break; + return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); case FileOperation.CREATE: - await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token); - break; - default: - //ignore, dont send + return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); } + return undefined; } - private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise { + private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, timeout: number, token: CancellationToken): Promise { + const extensionNames = new Set(); const edits: WorkspaceEdit[] = []; await emitter.fireAsync(data, token, async (thenable, listener) => { @@ -204,25 +201,28 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ const result = await Promise.resolve(thenable); if (result instanceof WorkspaceEdit) { edits.push(result); + extensionNames.add((>listener).extension.displayName ?? (>listener).extension.identifier.value); } if (Date.now() - now > timeout) { - this._logService.warn('SLOW file-participant', (>listener).extension?.identifier); + this._logService.warn('SLOW file-participant', (>listener).extension.identifier); } }); if (token.isCancellationRequested) { - return; + return undefined; } - if (edits.length > 0) { - // concat all WorkspaceEdits collected via waitUntil-call and apply them in one go. - const dto: IWorkspaceEditDto = { edits: [] }; - for (let edit of edits) { - let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - dto.edits = dto.edits.concat(edits); - } - return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto, undoRedoGroupId); + if (edits.length === 0) { + return undefined; } + + // concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer + const dto: IWorkspaceEditDto = { edits: [] }; + for (let edit of edits) { + let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + dto.edits = dto.edits.concat(edits); + } + return { edit: dto, extensionNames: Array.from(extensionNames) }; } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index f7f5a6f32e..49f65ee8ad 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -27,11 +27,12 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { IURITransformer } from 'vs/base/common/uriIpc'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto'; +import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { CancellationError } from 'vs/base/common/errors'; // --- adapter @@ -1062,6 +1063,20 @@ class SignatureHelpAdapter { } } +class InlineHintsAdapter { + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineHintsProvider, + ) { } + + provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + return asPromise(() => this._provider.provideInlineHints(doc, typeConvert.Range.to(range), token)).then(value => { + return value ? { hints: value.map(typeConvert.InlineHint.from) } : undefined; + }); + } +} + class LinkProviderAdapter { private _cache = new Cache('DocumentLink'); @@ -1320,7 +1335,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter - | LinkedEditingRangeAdapter; + | LinkedEditingRangeAdapter | InlineHintsAdapter; class AdapterData { constructor( @@ -1403,7 +1418,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ExtHostLanguageFeatures._handlePool++; } - private _withAdapter(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise, fallbackValue: R): Promise { + private _withAdapter(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise, fallbackValue: R, allowCancellationError: boolean = false): Promise { const data = this._adapter.get(handle); if (!data) { return Promise.resolve(fallbackValue); @@ -1421,8 +1436,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(p).then( () => this._logService.trace(`[${extension.identifier.value}] provider DONE after ${Date.now() - t1}ms`), err => { - this._logService.error(`[${extension.identifier.value}] provider FAILED`); - this._logService.error(err); + const isExpectedError = allowCancellationError && (err instanceof CancellationError); + if (!isExpectedError) { + this._logService.error(`[${extension.identifier.value}] provider FAILED`); + this._logService.error(err); + } } ); } @@ -1711,7 +1729,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null); + return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null, true); } $releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void { @@ -1725,7 +1743,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { - return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null); + return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null, true); } //#endregion @@ -1770,6 +1788,27 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.releaseSignatureHelp(id), undefined); } + // --- inline hints + + registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + + const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined; + const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension); + + this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + return result; + } + + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineHintsAdapter, adapter => adapter.provideInlineHints(URI.revive(resource), range, token), undefined); + } + // --- links registerDocumentLinkProvider(extension: IExtensionDescription | undefined, selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { @@ -1882,7 +1921,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return { beforeText: ExtHostLanguageFeatures._serializeRegExp(onEnterRule.beforeText), afterText: onEnterRule.afterText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.afterText) : undefined, - oneLineAboveText: onEnterRule.oneLineAboveText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.oneLineAboveText) : undefined, + previousLineText: onEnterRule.previousLineText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.previousLineText) : undefined, action: onEnterRule.action }; } diff --git a/src/vs/workbench/api/common/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts index 7f4627f9ab..95b2da7e23 100644 --- a/src/vs/workbench/api/common/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -8,6 +8,7 @@ import type * as vscode from 'vscode'; import { MainContext, MainThreadMessageServiceShape, MainThreadMessageOptions, IMainContext } from './extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; function isMessageItem(item: any): item is vscode.MessageItem { return item && item.title; @@ -37,9 +38,14 @@ export class ExtHostMessageService { items = [optionsOrFirstItem, ...rest]; } else { options.modal = optionsOrFirstItem && optionsOrFirstItem.modal; + options.useCustom = optionsOrFirstItem && optionsOrFirstItem.useCustom; items = rest; } + if (options.useCustom) { + checkProposedApiEnabled(extension); + } + const commands: { title: string; isCloseAffordance: boolean; handle: number; }[] = []; for (let handle = 0; handle < items.length; handle++) { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index abefbd3796..48fd4f0291 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -78,8 +78,8 @@ export interface ExtHostNotebookOutputRenderingHandler { } export class ExtHostNotebookKernelProviderAdapter extends Disposable { - private _kernelToId = new Map(); - private _idToKernel = new Map(); + private _kernelToFriendlyId = new Map(); + private _friendlyIdToKernel = new Map(); constructor( private readonly _proxy: MainThreadNotebookShape, private readonly _handle: number, @@ -101,24 +101,25 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const newMap = new Map(); let kernel_unique_pool = 0; - const kernelIdCache = new Set(); + const kernelFriendlyIdCache = new Set(); const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => { - let id = this._kernelToId.get(kernel); - if (id === undefined) { - if (kernel.id && kernelIdCache.has(kernel.id)) { - id = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; + let friendlyId = this._kernelToFriendlyId.get(kernel); + if (friendlyId === undefined) { + if (kernel.id && kernelFriendlyIdCache.has(kernel.id)) { + friendlyId = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`; } else { - id = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; + friendlyId = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`; } - this._kernelToId.set(kernel, id); + this._kernelToFriendlyId.set(kernel, friendlyId); } - newMap.set(kernel, id); + newMap.set(kernel, friendlyId); return { - id, + id: kernel.id, + friendlyId: friendlyId, label: kernel.label, extension: this._extension.identifier, extensionLocation: this._extension.extensionLocation, @@ -129,22 +130,22 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { }; }); - this._kernelToId = newMap; + this._kernelToFriendlyId = newMap; - this._idToKernel.clear(); - this._kernelToId.forEach((value, key) => { - this._idToKernel.set(value, key); + this._friendlyIdToKernel.clear(); + this._kernelToFriendlyId.forEach((value, key) => { + this._friendlyIdToKernel.set(value, key); }); return transformedData; } - getKernel(kernelId: string) { - return this._idToKernel.get(kernelId); + getKernelByFriendlyId(kernelId: string) { + return this._friendlyIdToKernel.get(kernelId); } async resolveNotebook(kernelId: string, document: ExtHostNotebookDocument, webview: vscode.NotebookCommunication, token: CancellationToken) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token); @@ -152,7 +153,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (!kernel) { return; @@ -166,7 +167,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) { - const kernel = this._idToKernel.get(kernelId); + const kernel = this._friendlyIdToKernel.get(kernelId); if (!kernel) { return; @@ -190,21 +191,22 @@ async function withToken(cb: (token: CancellationToken) => any) { } } -export class NotebookEditorDecorationType implements vscode.NotebookEditorDecorationType { +export class NotebookEditorDecorationType { private static readonly _Keys = new IdGenerator('NotebookEditorDecorationType'); - private _proxy: MainThreadNotebookShape; - public key: string; + readonly value: vscode.NotebookEditorDecorationType; constructor(proxy: MainThreadNotebookShape, options: vscode.NotebookDecorationRenderOptions) { - this.key = NotebookEditorDecorationType._Keys.nextId(); - this._proxy = proxy; - this._proxy.$registerNotebookEditorDecorationType(this.key, typeConverters.NotebookDecorationRenderOptions.from(options)); - } + const key = NotebookEditorDecorationType._Keys.nextId(); + proxy.$registerNotebookEditorDecorationType(key, typeConverters.NotebookDecorationRenderOptions.from(options)); - public dispose(): void { - this._proxy.$removeNotebookEditorDecorationType(this.key); + this.value = { + key, + dispose() { + proxy.$removeNotebookEditorDecorationType(key); + } + }; } } @@ -320,23 +322,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._notebookContentProviders.set(viewType, { extension, provider }); const listeners: vscode.Disposable[] = []; - listeners.push(provider.onDidChangeNotebook - ? provider.onDidChangeNotebook(e => { - const document = this._documents.get(URI.revive(e.document.uri)); - - if (!document) { - throw new Error(`Notebook document ${e.document.uri.toString()} not found`); - } - - if (isEditEvent(e)) { - const editId = document.addEdit(e); - this._proxy.$onUndoableContentChange(e.document.uri, viewType, editId, e.label); - } else { - this._proxy.$onContentChange(e.document.uri, viewType); - } - }) - : Disposable.None); - listeners.push(provider.onDidChangeNotebookContentOptions ? provider.onDidChangeNotebookContentOptions(() => { this._proxy.$updateNotebookProviderOptions(viewType, provider.options); @@ -383,7 +368,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { - return new NotebookEditorDecorationType(this._proxy, options); + return new NotebookEditorDecorationType(this._proxy, options).value; } async openNotebookDocument(uriComponents: UriComponents, viewType?: string): Promise { @@ -543,26 +528,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return false; } - async $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { - const document = this._documents.get(URI.revive(uri)); - if (!document) { - return; - } - - document.undo(editId, isDirty); - - } - - async $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise { - const document = this._documents.get(URI.revive(uri)); - if (!document) { - return; - } - - document.redo(editId, isDirty); - } - - async $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise { const document = this._documents.get(URI.revive(uri)); const provider = this._notebookContentProviders.get(viewType); @@ -580,10 +545,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._outputDisplayOrder = displayOrder; } - $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) { + $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }) { if (event.providerHandle !== undefined) { this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { - const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; + const kernel = event.kernelFriendlyId ? adapter.getKernelByFriendlyId(event.kernelFriendlyId) : undefined; this._editors.forEach(editor => { if (editor.editor.notebookData === document) { editor.editor._acceptKernel(kernel); @@ -891,11 +856,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } -function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocumentContentChangeEvent): e is vscode.NotebookDocumentEditEvent { - return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' - && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; -} - export class NotebookCellStatusBarItemInternal extends Disposable { private static NEXT_ID = 0; diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index af0c73ecf6..9f3ec868dd 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -15,7 +15,6 @@ import { CellKind, INotebookDocumentPropertiesChangeData, IWorkspaceCellEditDto, import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { CellEditType, CellOutputKind, diff, IMainCellDto, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; -import { Cache } from './cache'; interface IObservable { @@ -113,14 +112,17 @@ export class ExtHostCell extends Disposable { get cell(): vscode.NotebookCell { if (!this._cell) { const that = this; - const document = this._extHostDocument.getDocument(this.uri)!.document; + const data = this._extHostDocument.getDocument(this.uri); + if (!data) { + throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`); + } this._cell = Object.freeze({ get index() { return that._notebook.getCellIndex(that); }, notebook: that._notebook.notebookDocument, uri: that.uri, cellKind: this._cellData.cellKind, - document, - get language() { return document.languageId; }, + document: data.document, + get language() { return data!.document.languageId; }, get outputs() { return that._outputs; }, set outputs(value) { that._updateOutputs(value); }, get metadata() { return that._metadata; }, @@ -228,8 +230,6 @@ export class ExtHostNotebookDocument extends Disposable { private _disposed = false; private _languages: string[] = []; - private readonly _edits = new Cache('notebook documents'); - constructor( private readonly _proxy: MainThreadNotebookShape, private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, @@ -493,37 +493,4 @@ export class ExtHostNotebookDocument extends Disposable { getCellIndex(cell: ExtHostCell): number { return this._cells.indexOf(cell); } - - addEdit(item: vscode.NotebookDocumentEditEvent): number { - return this._edits.add([item]); - } - - async undo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).undo(); - // if (!isDirty) { - // this.disposeBackup(); - // } - } - - async redo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).redo(); - // if (!isDirty) { - // this.disposeBackup(); - // } - } - - private getEdit(editId: number): vscode.NotebookDocumentEditEvent { - const edit = this._edits.get(editId, 0); - if (!edit) { - throw new Error('No edit found'); - } - - return edit; - } - - disposeEdits(editIds: number[]): void { - for (const id of editIds) { - this._edits.delete(id); - } - } } diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index aff45bd47b..1892532582 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -208,7 +208,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook if (prev.editType === CellEditType.Replace && editData.cellEdits[i].editType === CellEditType.Replace) { const edit = editData.cellEdits[i]; - if ((edit.editType !== CellEditType.DocumentMetadata && edit.editType !== CellEditType.Unknown) && prev.index === edit.index) { + if ((edit.editType !== CellEditType.DocumentMetadata) && prev.index === edit.index) { prev.cells.push(...(editData.cellEdits[i] as ICellReplaceEdit).cells); prev.count += (editData.cellEdits[i] as ICellReplaceEdit).count; continue; diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 97078e0943..2d35af5cd6 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -233,6 +233,7 @@ class ExtHostQuickInput implements QuickInput { private static _nextId = 1; _id = ExtHostQuickPick._nextId++; + #proxy: MainThreadQuickOpenShape; private _title: string | undefined; private _steps: number | undefined; private _totalSteps: number | undefined; @@ -260,7 +261,8 @@ class ExtHostQuickInput implements QuickInput { this._onDidChangeValueEmitter ]; - constructor(protected _proxy: MainThreadQuickOpenShape, protected _extensionId: ExtensionIdentifier, private _onDidDispose: () => void) { + constructor(proxy: MainThreadQuickOpenShape, protected _extensionId: ExtensionIdentifier, private _onDidDispose: () => void) { + this.#proxy = proxy; } get title() { @@ -409,7 +411,7 @@ class ExtHostQuickInput implements QuickInput { this._updateTimeout = undefined; } this._onDidDispose(); - this._proxy.$dispose(this._id); + this.#proxy.$dispose(this._id); } protected update(properties: Record): void { @@ -437,7 +439,7 @@ class ExtHostQuickInput implements QuickInput { } private dispatchUpdate() { - this._proxy.$createOrUpdate(this._pendingUpdate); + this.#proxy.$createOrUpdate(this._pendingUpdate); this._pendingUpdate = { id: this._id }; } } diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 6abd4b7d0b..58a68e4a14 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as performance from 'vs/base/common/performance'; import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; @@ -55,7 +56,9 @@ export abstract class RequireInterceptor { this._installInterceptor(); + performance.mark('code/extHost/willWaitForConfig'); const configProvider = await this._extHostConfiguration.getConfigProvider(); + performance.mark('code/extHost/didWaitForConfig'); const extensionPaths = await this._extHostExtensionService.getExtensionPathIndex(); this.register(new VSCodeNodeModuleFactory(this._apiFactory.vscode, extensionPaths, this._extensionRegistry, configProvider, this._logService)); // {{SQL CARBON EDIT}} // add node module diff --git a/src/vs/workbench/api/common/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts new file mode 100644 index 0000000000..732952c004 --- /dev/null +++ b/src/vs/workbench/api/common/extHostSecrets.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; + +import { ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class ExtensionSecrets implements vscode.SecretStorage { + + protected readonly _id: string; + readonly #secretState: ExtHostSecretState; + + private _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + + constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) { + this._id = ExtensionIdentifier.toKey(extensionDescription.identifier); + this.#secretState = secretState; + + this.#secretState.onDidChangePassword(e => { + if (e.extensionId === this._id) { + this._onDidChange.fire({ key: e.key }); + } + }); + } + + get(key: string): Promise { + return this.#secretState.get(this._id, key); + } + + store(key: string, value: string): Promise { + return this.#secretState.store(this._id, key, value); + } + + delete(key: string): Promise { + return this.#secretState.delete(this._id, key); + } +} diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 6f3f5a1680..1599f2f74f 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -10,12 +10,16 @@ import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from import { localize } from 'vs/nls'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private static ID_GEN = 0; - private static ALLOWED_BACKGROUND_COLORS = new Set(['statusBarItem.errorBackground']); + + private static ALLOWED_BACKGROUND_COLORS = new Map( + [['statusBarItem.errorBackground', new ThemeColor('statusBarItem.errorForeground')]] + ); + + #proxy: MainThreadStatusBarShape; + #commands: CommandsConverter; private _id: number; private _alignment: number; @@ -37,21 +41,18 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { }; private _timeoutHandle: any; - private _proxy: MainThreadStatusBarShape; - private _commands: CommandsConverter; private _accessibilityInformation?: vscode.AccessibilityInformation; - private _extension?: IExtensionDescription; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { + this.#proxy = proxy; + this.#commands = commands; + this._id = ExtHostStatusBarEntry.ID_GEN++; - this._proxy = proxy; - this._commands = commands; this._statusId = id; this._statusName = name; this._alignment = alignment; this._priority = priority; this._accessibilityInformation = accessibilityInformation; - this._extension = extension; } public get id(): number { @@ -106,10 +107,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } public set backgroundColor(color: ThemeColor | undefined) { - if (this._extension) { - checkProposedApiEnabled(this._extension); - } - if (color && !ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.has(color.id)) { color = undefined; } @@ -127,12 +124,12 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { if (typeof command === 'string') { this._command = { fromApi: command, - internal: this._commands.toInternal({ title: '', command }, this._internalCommandRegistration), + internal: this.#commands.toInternal({ title: '', command }, this._internalCommandRegistration), }; } else if (command) { this._command = { fromApi: command, - internal: this._commands.toInternal(command, this._internalCommandRegistration), + internal: this.#commands.toInternal(command, this._internalCommandRegistration), }; } else { this._command = undefined; @@ -153,7 +150,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { public hide(): void { clearTimeout(this._timeoutHandle); this._visible = false; - this._proxy.$dispose(this.id); + this.#proxy.$dispose(this.id); } private update(): void { @@ -167,9 +164,15 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._timeoutHandle = setTimeout(() => { this._timeoutHandle = undefined; + // If a background color is set, the foreground is determined + let color = this._color; + if (this._backgroundColor) { + color = ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.get(this._backgroundColor.id); + } + // Set to status bar - this._proxy.$setEntry(this.id, this._statusId, this._statusName, this.text, this.tooltip, this._command?.internal, this.color, - this.backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, + this.#proxy.$setEntry(this.id, this._statusId, this._statusName, this._text, this._tooltip, this._command?.internal, color, + this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority, this._accessibilityInformation); }, 0); } @@ -230,8 +233,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation, extension); + createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/common/extHostStoragePaths.ts b/src/vs/workbench/api/common/extHostStoragePaths.ts index c4f1dbc33b..dfb400bb73 100644 --- a/src/vs/workbench/api/common/extHostStoragePaths.ts +++ b/src/vs/workbench/api/common/extHostStoragePaths.ts @@ -48,7 +48,7 @@ export class ExtensionStoragePaths implements IExtensionStoragePaths { const storageUri = URI.joinPath(this._environment.workspaceStorageHome, storageName); try { - await this._extHostFileSystem.stat(storageUri); + await this._extHostFileSystem.value.stat(storageUri); this._logService.trace('[ExtHostStorage] storage dir already exists', storageUri); return storageUri; } catch { @@ -57,8 +57,8 @@ export class ExtensionStoragePaths implements IExtensionStoragePaths { try { this._logService.trace('[ExtHostStorage] creating dir and metadata-file', storageUri); - await this._extHostFileSystem.createDirectory(storageUri); - await this._extHostFileSystem.writeFile( + await this._extHostFileSystem.value.createDirectory(storageUri); + await this._extHostFileSystem.value.writeFile( URI.joinPath(storageUri, 'meta.json'), new TextEncoder().encode(JSON.stringify({ id: this._workspace.id, diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 020e9d74ad..5f17f54c48 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -341,7 +341,10 @@ export namespace TaskFilterDTO { class TaskExecutionImpl implements vscode.TaskExecution { - constructor(private readonly _tasks: ExtHostTaskBase, readonly _id: string, private readonly _task: vscode.Task) { + readonly #tasks: ExtHostTaskBase; + + constructor(tasks: ExtHostTaskBase, readonly _id: string, private readonly _task: vscode.Task) { + this.#tasks = tasks; } public get task(): vscode.Task { @@ -349,7 +352,7 @@ class TaskExecutionImpl implements vscode.TaskExecution { } public terminate(): void { - this._tasks.terminateTask(this); + this.#tasks.terminateTask(this); } public fireDidStartProcess(value: tasks.TaskProcessStartedDTO): void { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 5b9faa25cf..982b97ed5e 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -5,12 +5,11 @@ import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ITerminalChildProcess, EXT_HOST_CREATION_DELAY, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; -import { timeout } from 'vs/base/common/async'; +import { ITerminalChildProcess, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -21,6 +20,7 @@ import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { generateUuid } from 'vs/base/common/uuid'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -47,63 +47,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); -export class BaseExtHostTerminal { - public _id: number | undefined; - protected _idPromise: Promise; - private _idPromiseComplete: ((value: number) => any) | undefined; +export class ExtHostTerminal { private _disposed: boolean = false; - private _queuedRequests: ApiRequest[] = []; - - constructor( - protected _proxy: MainThreadTerminalServiceShape, - id?: number - ) { - this._idPromise = new Promise(c => { - if (id !== undefined) { - this._id = id; - c(id); - } else { - this._idPromiseComplete = c; - } - }); - } - - public dispose(): void { - if (!this._disposed) { - this._disposed = true; - this._queueApiRequest(this._proxy.$dispose, []); - } - } - - protected _checkDisposed() { - if (this._disposed) { - throw new Error('Terminal has already been disposed'); - } - } - - protected _queueApiRequest(callback: (...args: any[]) => void, args: any[]): void { - const request: ApiRequest = new ApiRequest(callback, args); - if (!this._id) { - this._queuedRequests.push(request); - return; - } - request.run(this._proxy, this._id); - } - - public _runQueuedRequests(id: number): void { - this._id = id; - if (this._idPromiseComplete) { - this._idPromiseComplete(id); - this._idPromiseComplete = undefined; - } - this._queuedRequests.forEach((r) => { - r.run(this._proxy, id); - }); - this._queuedRequests.length = 0; - } -} - -export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal { private _pidPromise: Promise; private _cols: number | undefined; private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; @@ -112,15 +57,59 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi public isOpen: boolean = false; + readonly value: vscode.Terminal; + constructor( - proxy: MainThreadTerminalServiceShape, + private _proxy: MainThreadTerminalServiceShape, + public _id: TerminalIdentifier, private readonly _creationOptions: vscode.TerminalOptions | vscode.ExtensionTerminalOptions, private _name?: string, - id?: number ) { - super(proxy, id); this._creationOptions = Object.freeze(this._creationOptions); this._pidPromise = new Promise(c => this._pidPromiseComplete = c); + + const that = this; + this.value = { + get name(): string { + return that._name || ''; + }, + get processId(): Promise { + return that._pidPromise; + }, + get creationOptions(): Readonly { + return that._creationOptions; + }, + get exitStatus(): vscode.TerminalExitStatus | undefined { + return that._exitStatus; + }, + sendText(text: string, addNewLine: boolean = true): void { + that._checkDisposed(); + that._proxy.$sendText(that._id, text, addNewLine); + }, + show(preserveFocus: boolean): void { + that._checkDisposed(); + that._proxy.$show(that._id, preserveFocus); + }, + hide(): void { + that._checkDisposed(); + that._proxy.$hide(that._id); + }, + dispose(): void { + if (!that._disposed) { + that._disposed = true; + that._proxy.$dispose(that._id); + } + }, + get dimensions(): vscode.TerminalDimensions | undefined { + if (that._cols === undefined || that._rows === undefined) { + return undefined; + } + return { + columns: that._cols, + rows: that._rows + }; + } + }; } public async create( @@ -133,40 +122,34 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi hideFromUser?: boolean, isFeatureTerminal?: boolean ): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); - this._name = result.name; - this._runQueuedRequests(result.id); + if (typeof this._id !== 'string') { + throw new Error('Terminal has already been created'); + } + await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal }); } public async createExtensionTerminal(): Promise { - const result = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); - this._name = result.name; - this._runQueuedRequests(result.id); - return result.id; + if (typeof this._id !== 'string') { + throw new Error('Terminal has already been created'); + } + await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionTerminal: true }); + // At this point, the id has been set via `$acceptTerminalOpened` + if (typeof this._id === 'string') { + throw new Error('Terminal creation failed'); + } + return this._id; } - public get name(): string { - return this._name || ''; + private _checkDisposed() { + if (this._disposed) { + throw new Error('Terminal has already been disposed'); + } } public set name(name: string) { this._name = name; } - public get exitStatus(): vscode.TerminalExitStatus | undefined { - return this._exitStatus; - } - - public get dimensions(): vscode.TerminalDimensions | undefined { - if (this._cols === undefined || this._rows === undefined) { - return undefined; - } - return { - columns: this._cols, - rows: this._rows - }; - } - public setExitCode(code: number | undefined) { this._exitStatus = Object.freeze({ code }); } @@ -184,29 +167,6 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi return true; } - public get processId(): Promise { - return this._pidPromise; - } - - public get creationOptions(): Readonly { - return this._creationOptions; - } - - public sendText(text: string, addNewLine: boolean = true): void { - this._checkDisposed(); - this._queueApiRequest(this._proxy.$sendText, [text, addNewLine]); - } - - public show(preserveFocus: boolean): void { - this._checkDisposed(); - this._queueApiRequest(this._proxy.$show, [preserveFocus]); - } - - public hide(): void { - this._checkDisposed(); - this._queueApiRequest(this._proxy.$hide, []); - } - public _setProcessId(processId: number | undefined): void { // The event may fire 2 times when the panel is restored if (this._pidPromiseComplete) { @@ -223,20 +183,6 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi } } -class ApiRequest { - private _callback: (...args: any[]) => void; - private _args: any[]; - - constructor(callback: (...args: any[]) => void, args: any[]) { - this._callback = callback; - this._args = args; - } - - public run(proxy: MainThreadTerminalServiceShape, id: number) { - this._callback.apply(proxy, [id].concat(this._args)); - } -} - export class ExtHostPseudoterminal implements ITerminalChildProcess { private readonly _onProcessData = new Emitter(); public readonly onProcessData: Event = this._onProcessData.event; @@ -271,6 +217,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { } } + acknowledgeDataEvent(charCount: number): void { + // No-op, flow control is not supported in extension owned terminals. If this is ever + // implemented it will need new pause and resume VS Code APIs. + } + getInitialCwd(): Promise { return Promise.resolve(''); } @@ -296,6 +247,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { } this._pty.open(initialDimensions ? initialDimensions : undefined); + + if (this._pty.setDimensions && initialDimensions) { + this._pty.setDimensions(initialDimensions); + } + this._onProcessReady.fire({ pid: -1, cwd: '' }); } } @@ -325,8 +281,8 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); - public get activeTerminal(): ExtHostTerminal | undefined { return this._activeTerminal; } - public get terminals(): ExtHostTerminal[] { return this._terminals; } + public get activeTerminal(): vscode.Terminal | undefined { return this._activeTerminal?.value; } + public get terminals(): vscode.Terminal[] { return this._terminals.map(term => term.value); } protected readonly _onDidCloseTerminal: Emitter = new Emitter(); public get onDidCloseTerminal(): Event { return this._onDidCloseTerminal && this._onDidCloseTerminal.event; } @@ -370,18 +326,18 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, options, options.name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); terminal.createExtensionTerminal().then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); this._terminals.push(terminal); - return terminal; + return terminal.value; } public attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void { - const terminal = this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (!terminal) { throw new Error(`Cannot resolve terminal with id ${id} for virtual process`); } @@ -395,71 +351,71 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I if (id === null) { this._activeTerminal = undefined; if (original !== this._activeTerminal) { - this._onDidChangeActiveTerminal.fire(this._activeTerminal); + this._onDidChangeActiveTerminal.fire(this._activeTerminal.value); } return; } - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { this._activeTerminal = terminal; if (original !== this._activeTerminal) { - this._onDidChangeActiveTerminal.fire(this._activeTerminal); + this._onDidChangeActiveTerminal.fire(this._activeTerminal.value); } } } public async $acceptTerminalProcessData(id: number, data: string): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { - this._onDidWriteTerminalData.fire({ terminal, data }); + this._onDidWriteTerminalData.fire({ terminal: terminal.value, data }); } } public async $acceptTerminalDimensions(id: number, cols: number, rows: number): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { if (terminal.setDimensions(cols, rows)) { this._onDidChangeTerminalDimensions.fire({ - terminal: terminal, - dimensions: terminal.dimensions as vscode.TerminalDimensions + terminal: terminal.value, + dimensions: terminal.value.dimensions as vscode.TerminalDimensions }); } } } public async $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): Promise { - await this._getTerminalByIdEventually(id); - // Extension pty terminal only - when virtual process resize fires it means that the // terminal's maximum dimensions changed this._terminalProcesses.get(id)?.resize(cols, rows); } public async $acceptTerminalTitleChange(id: number, name: string): Promise { - await this._getTerminalByIdEventually(id); - const extHostTerminal = this._getTerminalObjectById(this.terminals, id); - if (extHostTerminal) { - extHostTerminal.name = name; + const terminal = this._getTerminalById(id); + if (terminal) { + terminal.name = name; } } public async $acceptTerminalClosed(id: number, exitCode: number | undefined): Promise { - await this._getTerminalByIdEventually(id); - const index = this._getTerminalObjectIndexById(this.terminals, id); + const index = this._getTerminalObjectIndexById(this._terminals, id); if (index !== null) { const terminal = this._terminals.splice(index, 1)[0]; terminal.setExitCode(exitCode); - this._onDidCloseTerminal.fire(terminal); + this._onDidCloseTerminal.fire(terminal.value); } } - public $acceptTerminalOpened(id: number, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void { - const index = this._getTerminalObjectIndexById(this._terminals, id); - if (index !== null) { - // The terminal has already been created (via createTerminal*), only fire the event - this._onDidOpenTerminal.fire(this.terminals[index]); - this.terminals[index].isOpen = true; - return; + public $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void { + if (extHostTerminalId) { + // Resolve with the renderer generated id + const index = this._getTerminalObjectIndexById(this._terminals, extHostTerminalId); + if (index !== null) { + // The terminal has already been created (via createTerminal*), only fire the event + this._terminals[index]._id = id; + this._onDidOpenTerminal.fire(this.terminals[index]); + this._terminals[index].isOpen = true; + return; + } } const creationOptions: vscode.TerminalOptions = { @@ -470,14 +426,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I env: shellLaunchConfigDto.env, hideFromUser: shellLaunchConfigDto.hideFromUser }; - const terminal = new ExtHostTerminal(this._proxy, creationOptions, name, id); + const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); - this._onDidOpenTerminal.fire(terminal); + this._onDidOpenTerminal.fire(terminal.value); terminal.isOpen = true; } public async $acceptTerminalProcessId(id: number, processId: number): Promise { - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (terminal) { terminal._setProcessId(processId); } @@ -486,7 +442,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { // Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call // Pseudoterminal.start - const terminal = await this._getTerminalByIdEventually(id); + const terminal = this._getTerminalById(id); if (!terminal) { return { message: localize('launchFail.idMissingOnExtHost', "Could not find the terminal with id {0} on the extension host", id) }; } @@ -496,7 +452,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I await new Promise(r => { // Ensure open is called after onDidOpenTerminal const listener = this.onDidOpenTerminal(async e => { - if (e === terminal) { + if (e === terminal.value) { listener.dispose(); r(); } @@ -539,6 +495,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return disposables; } + public $acceptProcessAckDataEvent(id: number, charCount: number): void { + this._terminalProcesses.get(id)?.acknowledgeDataEvent(charCount); + } + public $acceptProcessInput(id: number, data: string): void { this._terminalProcesses.get(id)?.input(data); } @@ -601,7 +561,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._terminalLinkCancellationSource.set(terminalId, cancellationSource); const result: ITerminalLinkDto[] = []; - const context: vscode.TerminalLinkContext = { terminal, line }; + const context: vscode.TerminalLinkContext = { terminal: terminal.value, line }; const promises: vscode.ProviderResult<{ provider: vscode.TerminalLinkProvider, links: vscode.TerminalLink[] }>[] = []; for (const provider of this._linkProviders) { @@ -670,32 +630,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._proxy.$sendProcessExit(id, exitCode); } - // TODO: This could be improved by using a single promise and resolve it when the terminal is ready - private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { - if (!this._getTerminalPromises[id]) { - this._getTerminalPromises[id] = this._createGetTerminalPromise(id, retries); - } - return this._getTerminalPromises[id]; - } - - private _createGetTerminalPromise(id: number, retries: number = 5): Promise { - return new Promise(c => { - if (retries === 0) { - c(undefined); - return; - } - - const terminal = this._getTerminalById(id); - if (terminal) { - c(terminal); - } else { - // This should only be needed immediately after createTerminalRenderer is called as - // the ExtHostTerminal has not yet been iniitalized - timeout(EXT_HOST_CREATION_DELAY * 2).then(() => c(this._createGetTerminalPromise(id, retries - 1))); - } - }); - } - private _getTerminalById(id: number): ExtHostTerminal | null { return this._getTerminalObjectById(this._terminals, id); } @@ -705,7 +639,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return index !== null ? array[index] : null; } - private _getTerminalObjectIndexById(array: T[], id: number): number | null { + private _getTerminalObjectIndexById(array: T[], id: TerminalIdentifier): number | null { let index: number | null = null; array.some((item, i) => { const thisId = item._id; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 514ea30582..17c9f8c41b 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -6,7 +6,6 @@ import { mapFind } from 'vs/base/common/arrays'; import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { throttle } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; @@ -14,25 +13,35 @@ import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters'; -import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; +import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; export class ExtHostTesting implements ExtHostTestingShape { + private readonly resultsChangedEmitter = new Emitter(); private readonly providers = new Map(); private readonly proxy: MainThreadTestingShape; private readonly ownedTests = new OwnedTestCollection(); - private readonly testSubscriptions = new Map(); + private readonly testSubscriptions = new Map void; + }>(); private workspaceObservers: WorkspaceFolderTestObserverFactory; private textDocumentObservers: TextDocumentTestObserverFactory; + public onLastResultsChanged = this.resultsChangedEmitter.event; + public lastResults?: vscode.TestResults; + constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) { this.proxy = rpc.getProxy(MainContext.MainThreadTesting); this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy); @@ -47,6 +56,14 @@ export class ExtHostTesting implements ExtHostTestingShape { this.providers.set(providerId, provider); this.proxy.$registerTestProvider(providerId); + // give the ext a moment to register things rather than synchronously invoking within activate() + const toSubscribe = [...this.testSubscriptions.keys()]; + setTimeout(() => { + for (const subscription of toSubscribe) { + this.testSubscriptions.get(subscription)?.subscribeFn(providerId, provider); + } + }, 0); + return new Disposable(() => { this.providers.delete(providerId); this.proxy.$unregisterTestProvider(providerId); @@ -70,7 +87,7 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Implements vscode.test.runTests */ - public async runTests(req: vscode.TestRunOptions) { + public async runTests(req: vscode.TestRunOptions, token = CancellationToken.None) { await this.proxy.$runTests({ tests: req.tests // Find workspace items first, then owned tests, then document tests. @@ -82,14 +99,27 @@ export class ExtHostTesting implements ExtHostTestingShape { .filter(isDefined) .map(item => ({ providerId: item.providerId, testId: item.id })), debug: req.debug - }); + }, token); + } + + + /** + * Updates test results shown to extensions. + * @override + */ + public $publishTestResults(results: InternalTestResults): void { + const convert = (item: InternalTestItemWithChildren): vscode.RequiredTestItem => + ({ ...TestItem.toShallow(item.item), children: item.children.map(convert) }); + + this.lastResults = { tests: results.tests.map(convert) }; + this.resultsChangedEmitter.fire(); } /** * Handles a request to read tests for a file, or workspace. * @override */ - public $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { + public async $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const subscriptionKey = getTestSubscriptionKey(resource, uri); if (this.testSubscriptions.has(subscriptionKey)) { @@ -98,12 +128,29 @@ export class ExtHostTesting implements ExtHostTestingShape { let method: undefined | ((p: vscode.TestProvider) => vscode.TestHierarchy | undefined); if (resource === ExtHostTestingResource.TextDocument) { - const document = this.documents.getDocument(uri); + let document = this.documents.getDocument(uri); + + // we can ask to subscribe to tests before the documents are populated in + // the extension host. Try to wait. + if (!document) { + const store = new DisposableStore(); + document = await new Promise(resolve => { + store.add(disposableTimeout(() => resolve(undefined), 5000)); + store.add(this.documents.onDidAddDocuments(e => { + const data = e.find(data => data.document.uri.toString() === uri.toString()); + if (data) { resolve(data); } + })); + }).finally(() => store.dispose()); + } + if (document) { - method = p => p.createDocumentTestHierarchy?.(document.document); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); + method = p => p.createDocumentTestHierarchy + ? p.createDocumentTestHierarchy(document!.document) + : this.createDefaultDocumentTestHierarchy(p, document!.document, folder); } } else { - const folder = this.workspace.getWorkspaceFolder(uri, false); + const folder = await this.workspace.getWorkspaceFolder2(uri, false); if (folder) { method = p => p.createWorkspaceTestHierarchy?.(folder); } @@ -113,24 +160,34 @@ export class ExtHostTesting implements ExtHostTestingShape { return; } - const disposable = new DisposableStore(); - const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff))); - for (const [id, provider] of this.providers) { + const subscribeFn = (id: string, provider: vscode.TestProvider) => { try { - const hierarchy = method(provider); + const hierarchy = method!(provider); if (!hierarchy) { - continue; + return; } + collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, 1]); disposable.add(hierarchy); collection.addRoot(hierarchy.root, id); + Promise.resolve(hierarchy.discoveredInitialTests).then(() => collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, -1])); hierarchy.onDidChangeTest(e => collection.onItemChange(e, id)); } catch (e) { console.error(e); } + }; + + const disposable = new DisposableStore(); + const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff))); + for (const [id, provider] of this.providers) { + subscribeFn(id, provider); } - this.testSubscriptions.set(subscriptionKey, { store: disposable, collection }); + // note: we don't increment the root count initially -- this is done by the + // main thread, incrementing once per extension host. We just push the + // diff to signal that roots have been discovered. + collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]); + this.testSubscriptions.set(subscriptionKey, { store: disposable, collection, subscribeFn }); } /** @@ -162,217 +219,193 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest): Promise { + public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { const provider = this.providers.get(req.providerId); if (!provider || !provider.runTests) { return EMPTY_TEST_RESULT; } - const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual).filter(isDefined); + const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual) + .filter(isDefined) + // Only send the actual TestItem's to the user to run. + .map(t => t instanceof TestItemFilteredWrapper ? t.actual : t); if (!tests.length) { return EMPTY_TEST_RESULT; } - await provider.runTests({ tests, debug: req.debug }, CancellationToken.None); - return EMPTY_TEST_RESULT; + try { + await provider.runTests({ tests, debug: req.debug }, cancellation); + for (const { collection } of this.testSubscriptions.values()) { + collection.flushDiff(); // ensure all states are updated + } + + return EMPTY_TEST_RESULT; + } catch (e) { + console.error(e); // so it appears to attached debuggers + throw e; + } + } + + public $lookupTest(req: TestIdWithProvider): Promise { + const owned = this.ownedTests.getTestById(req.testId); + if (!owned) { + return Promise.resolve(undefined); + } + + const { actual, previousChildren, previousEquals, ...item } = owned; + return Promise.resolve(item); + } + + private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy | undefined { + if (!folder) { + return undefined; + } + + const workspaceHierarchy = provider.createWorkspaceTestHierarchy?.(folder); + if (!workspaceHierarchy) { + return undefined; + } + + const onDidChangeTest = new Emitter(); + workspaceHierarchy.onDidChangeTest(node => { + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(node, document); + const previouslySeen = wrapper.hasNodeMatchingFilter; + + if (previouslySeen) { + // reset cache and get whether you can currently see the TestItem. + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; + + if (currentlySeen) { + onDidChangeTest.fire(wrapper); + return; + } + + // Fire the event to say that the current visible parent has changed. + onDidChangeTest.fire(wrapper.visibleParent); + return; + } + + const previousParent = wrapper.visibleParent; + wrapper.reset(); + const currentlySeen = wrapper.hasNodeMatchingFilter; + + // It wasn't previously seen and isn't currently seen so + // nothing has actually changed. + if (!currentlySeen) { + return; + } + + // The test is now visible so we need to refresh the cache + // of the previous visible parent and fire that it has changed. + previousParent.reset(); + onDidChangeTest.fire(previousParent); + }); + + return { + root: TestItemFilteredWrapper.getWrapperForTestItem(workspaceHierarchy.root, document), + dispose: () => { + onDidChangeTest.dispose(); + TestItemFilteredWrapper.removeFilter(document); + }, + onDidChangeTest: onDidChangeTest.event + }; } } -const keyMap: { [K in keyof Omit]: null } = { - label: null, - location: null, - state: null, - debuggable: null, - description: null, - runnable: null -}; - -const simpleProps = Object.keys(keyMap) as ReadonlyArray; - -const itemEqualityComparator = (a: vscode.TestItem) => { - const values: unknown[] = []; - for (const prop of simpleProps) { - values.push(a[prop]); - } - - return (b: vscode.TestItem) => { - for (let i = 0; i < simpleProps.length; i++) { - if (values[i] !== b[simpleProps[i]]) { - return false; - } - } - - return true; - }; -}; - -/** - * @private +/* + * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children + * to only the children that are located in a certain vscode.Uri. */ -export interface OwnedCollectionTestItem extends InternalTestItem { - actual: vscode.TestItem; - previousChildren: Set; - previousEquals: (v: vscode.TestItem) => boolean; -} - -/** - * @private - */ -export class OwnedTestCollection { - protected readonly testIdToInternal = new Map(); - - /** - * Gets test information by ID, if it was defined and still exists in this - * extension host. - */ - public getTestById(id: string) { - return this.testIdToInternal.get(id); +export class TestItemFilteredWrapper implements vscode.TestItem { + private static wrapperMap = new WeakMap>(); + public static removeFilter(document: vscode.TextDocument): void { + this.wrapperMap.delete(document); } - /** - * Creates a new test collection for a specific hierarchy for a workspace - * or document observation. - */ - public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) { - return new SingleUseTestCollection(this.testIdToInternal, publishDiff); - } -} - -/** - * Maintains tests created and registered for a single set of hierarchies - * for a workspace or document. - * @private - */ -export class SingleUseTestCollection implements IDisposable { - protected readonly testItemToInternal = new Map(); - protected diff: TestsDiff = []; - private disposed = false; - - constructor(private readonly testIdToInternal: Map, private readonly publishDiff: (diff: TestsDiff) => void) { } - - /** - * Adds a new root node to the collection. - */ - public addRoot(item: vscode.TestItem, providerId: string) { - this.addItem(item, providerId, null); - this.throttleSendDiff(); - } - - /** - * Gets test information by its reference, if it was defined and still exists - * in this extension host. - */ - public getTestByReference(item: vscode.TestItem) { - return this.testItemToInternal.get(item); - } - - /** - * Should be called when an item change is fired on the test provider. - */ - public onItemChange(item: vscode.TestItem, providerId: string) { - const existing = this.testItemToInternal.get(item); - if (!existing) { - if (!this.disposed) { - console.warn(`Received a TestProvider.onDidChangeTest for a test that wasn't seen before as a child.`); - } - return; + // Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists. + public static getWrapperForTestItem(item: vscode.TestItem, filterDocument: vscode.TextDocument, parent?: TestItemFilteredWrapper): TestItemFilteredWrapper { + let innerMap = this.wrapperMap.get(filterDocument); + if (innerMap?.has(item)) { + return innerMap.get(item)!; } - this.addItem(item, providerId, existing.parent); - this.throttleSendDiff(); - } + if (!innerMap) { + innerMap = new WeakMap(); + this.wrapperMap.set(filterDocument, innerMap); - /** - * Gets a diff of all changes that have been made, and clears the diff queue. - */ - public collectDiff() { - const diff = this.diff; - this.diff = []; - return diff; - } - - public dispose() { - for (const item of this.testItemToInternal.values()) { - this.testIdToInternal.delete(item.id); } - this.testIdToInternal.clear(); - this.diff = []; - this.disposed = true; + const w = new TestItemFilteredWrapper(item, filterDocument, parent); + innerMap.set(item, w); + return w; } - protected getId(): string { - return generateUuid(); + public get label() { + return this.actual.label; } - private addItem(actual: vscode.TestItem, providerId: string, parent: string | null) { - let internal = this.testItemToInternal.get(actual); - if (!internal) { - internal = { - actual, - id: this.getId(), - parent, - item: TestItem.from(actual), - providerId, - previousChildren: new Set(), - previousEquals: itemEqualityComparator(actual), - }; - - this.testItemToInternal.set(actual, internal); - this.testIdToInternal.set(internal.id, internal); - this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]); - } else if (!internal.previousEquals(actual)) { - internal.item = TestItem.from(actual); - internal.previousEquals = itemEqualityComparator(actual); - this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]); - } - - // If there are children, track which ones are deleted - // and recursively and/update them. - if (actual.children) { - const deletedChildren = internal.previousChildren; - const currentChildren = new Set(); - for (const child of actual.children) { - const c = this.addItem(child, providerId, internal.id); - deletedChildren.delete(c.id); - currentChildren.add(c.id); - } - - for (const child of deletedChildren) { - this.removeItembyId(child); - } - - internal.previousChildren = currentChildren; - } - - - return internal; + public get debuggable() { + return this.actual.debuggable; } - private removeItembyId(id: string) { - this.diff.push([TestDiffOpType.Remove, id]); - - const queue = [this.testIdToInternal.get(id)]; - while (queue.length) { - const item = queue.pop(); - if (!item) { - continue; - } - - this.testIdToInternal.delete(item.id); - this.testItemToInternal.delete(item.actual); - for (const child of item.previousChildren) { - queue.push(this.testIdToInternal.get(child)); - } - } + public get description() { + return this.actual.description; } - @throttle(200) - protected throttleSendDiff() { - const diff = this.collectDiff(); - if (diff.length) { - this.publishDiff(diff); + public get location() { + return this.actual.location; + } + + public get runnable() { + return this.actual.runnable; + } + + public get state() { + return this.actual.state; + } + + public get children() { + // We only want children that match the filter. + return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter); + } + + public get visibleParent(): TestItemFilteredWrapper { + return this.hasNodeMatchingFilter ? this : this.parent!.visibleParent; + } + + private matchesFilter: boolean | undefined; + + // Determines if the TestItem matches the filter. This would be true if: + // 1. We don't have a parent (because the root is the workspace root node) + // 2. The URI of the current node matches the filter URI + // 3. Some child of the current node matches the filter URI + public get hasNodeMatchingFilter(): boolean { + if (this.matchesFilter === undefined) { + this.matchesFilter = !this.parent + || this.actual.location?.uri.toString() === this.filterDocument.uri.toString() + || this.getWrappedChildren().some(child => child.hasNodeMatchingFilter); } + + return this.matchesFilter; + } + + // Reset the cache of whether or not you can see a node from a particular node + // up to it's visible parent. + public reset(): void { + if (this !== this.visibleParent) { + this.parent?.reset(); + } + this.matchesFilter = undefined; + } + + + private constructor(public readonly actual: vscode.TestItem, private filterDocument: vscode.TextDocument, private readonly parent?: TestItemFilteredWrapper) { + this.getWrappedChildren(); + } + + private getWrappedChildren() { + return this.actual.children?.map(t => TestItemFilteredWrapper.getWrapperForTestItem(t, this.filterDocument, this)) || []; } } @@ -382,7 +415,7 @@ export class SingleUseTestCollection implements IDisposable { interface MirroredCollectionTestItem extends IncrementalTestCollectionItem { revived: vscode.TestItem; depth: number; - wrapped?: vscode.TestItem; + wrapped?: vscode.RequiredTestItem; } class MirroredChangeCollector extends IncrementalChangeCollector { @@ -411,7 +444,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector): vscode.TestItem[] { - let output: vscode.TestItem[] = []; + public getAllAsTestItem(itemIds: Iterable): vscode.RequiredTestItem[] { + let output: vscode.RequiredTestItem[] = []; for (const itemId of itemIds) { const item = this.items.get(itemId); if (item) { @@ -577,7 +610,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection { const MirroredItemId = Symbol('MirroredItemId'); -class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { +class TestItemFromMirror implements vscode.RequiredTestItem { readonly #internal: MirroredCollectionTestItem; readonly #collection: MirroredTestCollection; + public get id() { return this.#internal.revived.id!; } public get label() { return this.#internal.revived.label; } public get description() { return this.#internal.revived.description; } public get state() { return this.#internal.revived.state; } @@ -627,14 +661,18 @@ class ExtHostTestItem implements vscode.TestItem, RequiredTestItem { } public toJSON() { - const serialized: RequiredTestItem = { + const serialized: vscode.RequiredTestItem & TestIdWithProvider = { + id: this.id, label: this.label, description: this.description, state: this.state, location: this.location, runnable: this.runnable, debuggable: this.debuggable, - children: this.children.map(c => (c as ExtHostTestItem).toJSON()), + children: this.children.map(c => (c as TestItemFromMirror).toJSON()), + + providerId: this.#internal.providerId, + testId: this.#internal.id, }; return serialized; @@ -655,6 +693,7 @@ abstract class AbstractTestObserverFactory { const resourceKey = resourceUri.toString(); const resource = this.resources.get(resourceKey) ?? this.createObserverData(resourceUri); + resource.pendingDeletion?.dispose(); resource.observers++; return { @@ -778,16 +817,9 @@ class TextDocumentTestObserverFactory extends AbstractTestObserverFactory { const uriString = resourceUri.toString(); this.diffListeners.set(uriString, onDiff); - const disposeListener = this.documents.onDidRemoveDocuments(evt => { - if (evt.some(delta => delta.document.uri.toString() === uriString)) { - this.unlisten(resourceUri); - } - }); - this.proxy.$subscribeToDiffs(ExtHostTestingResource.TextDocument, resourceUri); return new Disposable(() => { this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.TextDocument, resourceUri); - disposeListener.dispose(); this.diffListeners.delete(uriString); }); } diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 6aa3787c9d..20634cdcb1 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -10,28 +10,29 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { IRange } from 'vs/editor/common/core/range'; import { ISingleEditOperation } from 'vs/editor/common/model'; import { IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { EndOfLine, Position, Range, Selection, SnippetString, TextEditorLineNumbersStyle, TextEditorRevealType } from 'vs/workbench/api/common/extHostTypes'; import type * as vscode from 'vscode'; import { ILogService } from 'vs/platform/log/common/log'; +import { Lazy } from 'vs/base/common/lazy'; -export class TextEditorDecorationType implements vscode.TextEditorDecorationType { +export class TextEditorDecorationType { private static readonly _Keys = new IdGenerator('TextEditorDecorationType'); - private _proxy: MainThreadTextEditorsShape; - public key: string; + readonly value: vscode.TextEditorDecorationType; constructor(proxy: MainThreadTextEditorsShape, options: vscode.DecorationRenderOptions) { - this.key = TextEditorDecorationType._Keys.nextId(); - this._proxy = proxy; - this._proxy.$registerTextEditorDecorationType(this.key, TypeConverters.DecorationRenderOptions.from(options)); + const key = TextEditorDecorationType._Keys.nextId(); + proxy.$registerTextEditorDecorationType(key, TypeConverters.DecorationRenderOptions.from(options)); + this.value = Object.freeze({ + key, + dispose() { + proxy.$removeTextEditorDecorationType(key); + } + }); } - public dispose(): void { - this._proxy.$removeTextEditorDecorationType(this.key); - } } export interface ITextEditOperation { @@ -134,36 +135,63 @@ export class TextEditorEdit { } } -export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { +export class ExtHostTextEditorOptions { private _proxy: MainThreadTextEditorsShape; private _id: string; private _logService: ILogService; private _tabSize!: number; - private _indentSize!: number; private _insertSpaces!: boolean; private _cursorStyle!: TextEditorCursorStyle; private _lineNumbers!: TextEditorLineNumbersStyle; + readonly value: vscode.TextEditorOptions; + constructor(proxy: MainThreadTextEditorsShape, id: string, source: IResolvedTextEditorConfiguration, logService: ILogService) { this._proxy = proxy; this._id = id; this._accept(source); this._logService = logService; + + const that = this; + + this.value = { + get tabSize(): number | string { + return that._tabSize; + }, + set tabSize(value: number | string) { + that._setTabSize(value); + }, + get insertSpaces(): boolean | string { + return that._insertSpaces; + }, + set insertSpaces(value: boolean | string) { + that._setInsertSpaces(value); + }, + get cursorStyle(): TextEditorCursorStyle { + return that._cursorStyle; + }, + set cursorStyle(value: TextEditorCursorStyle) { + that._setCursorStyle(value); + }, + get lineNumbers(): TextEditorLineNumbersStyle { + return that._lineNumbers; + }, + set lineNumbers(value: TextEditorLineNumbersStyle) { + that._setLineNumbers(value); + } + }; } public _accept(source: IResolvedTextEditorConfiguration): void { this._tabSize = source.tabSize; - this._indentSize = source.indentSize; this._insertSpaces = source.insertSpaces; this._cursorStyle = source.cursorStyle; this._lineNumbers = TypeConverters.TextEditorLineNumbersStyle.to(source.lineNumbers); } - public get tabSize(): number | string { - return this._tabSize; - } + // --- internal: tabSize private _validateTabSize(value: number | string): number | 'auto' | null { if (value === 'auto') { @@ -183,7 +211,7 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { return null; } - public set tabSize(value: number | string) { + private _setTabSize(value: number | string) { const tabSize = this._validateTabSize(value); if (tabSize === null) { // ignore invalid call @@ -202,50 +230,7 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { })); } - public get indentSize(): number | string { - return this._indentSize; - } - - private _validateIndentSize(value: number | string): number | 'tabSize' | null { - if (value === 'tabSize') { - return 'tabSize'; - } - if (typeof value === 'number') { - const r = Math.floor(value); - return (r > 0 ? r : null); - } - if (typeof value === 'string') { - const r = parseInt(value, 10); - if (isNaN(r)) { - return null; - } - return (r > 0 ? r : null); - } - return null; - } - - public set indentSize(value: number | string) { - const indentSize = this._validateIndentSize(value); - if (indentSize === null) { - // ignore invalid call - return; - } - if (typeof indentSize === 'number') { - if (this._indentSize === indentSize) { - // nothing to do - return; - } - // reflect the new indentSize value immediately - this._indentSize = indentSize; - } - this._warnOnError(this._proxy.$trySetOptions(this._id, { - indentSize: indentSize - })); - } - - public get insertSpaces(): boolean | string { - return this._insertSpaces; - } + // --- internal: insert spaces private _validateInsertSpaces(value: boolean | string): boolean | 'auto' { if (value === 'auto') { @@ -254,7 +239,7 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { return (value === 'false' ? false : Boolean(value)); } - public set insertSpaces(value: boolean | string) { + private _setInsertSpaces(value: boolean | string) { const insertSpaces = this._validateInsertSpaces(value); if (typeof insertSpaces === 'boolean') { if (this._insertSpaces === insertSpaces) { @@ -269,11 +254,9 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { })); } - public get cursorStyle(): TextEditorCursorStyle { - return this._cursorStyle; - } + // --- internal: cursor style - public set cursorStyle(value: TextEditorCursorStyle) { + private _setCursorStyle(value: TextEditorCursorStyle) { if (this._cursorStyle === value) { // nothing to do return; @@ -284,11 +267,9 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { })); } - public get lineNumbers(): TextEditorLineNumbersStyle { - return this._lineNumbers; - } + // --- internal: line number - public set lineNumbers(value: TextEditorLineNumbersStyle) { + private _setLineNumbers(value: TextEditorLineNumbersStyle) { if (this._lineNumbers === value) { // nothing to do return; @@ -368,31 +349,170 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { } } -export class ExtHostTextEditor implements vscode.TextEditor { - - private readonly _documentData: ExtHostDocumentData; +export class ExtHostTextEditor { private _selections: Selection[]; private _options: ExtHostTextEditorOptions; private _visibleRanges: Range[]; private _viewColumn: vscode.ViewColumn | undefined; private _disposed: boolean = false; - private _hasDecorationsForKey: { [key: string]: boolean; }; + private _hasDecorationsForKey = new Set(); + + readonly value: vscode.TextEditor; constructor( readonly id: string, private readonly _proxy: MainThreadTextEditorsShape, private readonly _logService: ILogService, - document: ExtHostDocumentData, + document: Lazy, selections: Selection[], options: IResolvedTextEditorConfiguration, visibleRanges: Range[], viewColumn: vscode.ViewColumn | undefined ) { - this._documentData = document; this._selections = selections; this._options = new ExtHostTextEditorOptions(this._proxy, this.id, options, _logService); this._visibleRanges = visibleRanges; this._viewColumn = viewColumn; - this._hasDecorationsForKey = Object.create(null); + + const that = this; + + this.value = Object.freeze({ + get document(): vscode.TextDocument { + return document.getValue(); + }, + set document(_value) { + throw readonly('document'); + }, + // --- selection + get selection(): Selection { + return that._selections && that._selections[0]; + }, + set selection(value: Selection) { + if (!(value instanceof Selection)) { + throw illegalArgument('selection'); + } + that._selections = [value]; + that._trySetSelection(); + }, + get selections(): Selection[] { + return that._selections; + }, + set selections(value: Selection[]) { + if (!Array.isArray(value) || value.some(a => !(a instanceof Selection))) { + throw illegalArgument('selections'); + } + that._selections = value; + that._trySetSelection(); + }, + // --- visible ranges + get visibleRanges(): Range[] { + return that._visibleRanges; + }, + set visibleRanges(_value: Range[]) { + throw readonly('visibleRanges'); + }, + // --- options + get options(): vscode.TextEditorOptions { + return that._options.value; + }, + set options(value: vscode.TextEditorOptions) { + if (!that._disposed) { + that._options.assign(value); + } + }, + // --- view column + get viewColumn(): vscode.ViewColumn | undefined { + return that._viewColumn; + }, + set viewColumn(_value) { + throw readonly('viewColumn'); + }, + // --- edit + edit(callback: (edit: TextEditorEdit) => void, options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Promise { + if (that._disposed) { + return Promise.reject(new Error('TextEditor#edit not possible on closed editors')); + } + const edit = new TextEditorEdit(document.getValue(), options); + callback(edit); + return that._applyEdit(edit); + }, + // --- snippet edit + insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Promise { + if (that._disposed) { + return Promise.reject(new Error('TextEditor#insertSnippet not possible on closed editors')); + } + let ranges: IRange[]; + + if (!where || (Array.isArray(where) && where.length === 0)) { + ranges = that._selections.map(range => TypeConverters.Range.from(range)); + + } else if (where instanceof Position) { + const { lineNumber, column } = TypeConverters.Position.from(where); + ranges = [{ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column }]; + + } else if (where instanceof Range) { + ranges = [TypeConverters.Range.from(where)]; + } else { + ranges = []; + for (const posOrRange of where) { + if (posOrRange instanceof Range) { + ranges.push(TypeConverters.Range.from(posOrRange)); + } else { + const { lineNumber, column } = TypeConverters.Position.from(posOrRange); + ranges.push({ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column }); + } + } + } + return _proxy.$tryInsertSnippet(id, snippet.value, ranges, options); + }, + setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { + const willBeEmpty = (ranges.length === 0); + if (willBeEmpty && !that._hasDecorationsForKey.has(decorationType.key)) { + // avoid no-op call to the renderer + return; + } + if (willBeEmpty) { + that._hasDecorationsForKey.delete(decorationType.key); + } else { + that._hasDecorationsForKey.add(decorationType.key); + } + that._runOnProxy(() => { + if (TypeConverters.isDecorationOptionsArr(ranges)) { + return _proxy.$trySetDecorations( + id, + decorationType.key, + TypeConverters.fromRangeOrRangeWithMessage(ranges) + ); + } else { + const _ranges: number[] = new Array(4 * ranges.length); + for (let i = 0, len = ranges.length; i < len; i++) { + const range = ranges[i]; + _ranges[4 * i] = range.start.line + 1; + _ranges[4 * i + 1] = range.start.character + 1; + _ranges[4 * i + 2] = range.end.line + 1; + _ranges[4 * i + 3] = range.end.character + 1; + } + return _proxy.$trySetDecorationsFast( + id, + decorationType.key, + _ranges + ); + } + }); + }, + revealRange(range: Range, revealType: vscode.TextEditorRevealType): void { + that._runOnProxy(() => _proxy.$tryRevealRange( + id, + TypeConverters.Range.from(range), + (revealType || TextEditorRevealType.Default) + )); + }, + show(column: vscode.ViewColumn) { + _proxy.$tryShowEditor(id, TypeConverters.ViewColumn.from(column)); + }, + hide() { + _proxy.$tryHideEditor(id); + } + }); } dispose() { @@ -400,164 +520,32 @@ export class ExtHostTextEditor implements vscode.TextEditor { this._disposed = true; } - show(column: vscode.ViewColumn) { - this._proxy.$tryShowEditor(this.id, TypeConverters.ViewColumn.from(column)); - } - - hide() { - this._proxy.$tryHideEditor(this.id); - } - - // ---- the document - - get document(): vscode.TextDocument { - return this._documentData.document; - } - - set document(value) { - throw readonly('document'); - } - - // ---- options - - get options(): vscode.TextEditorOptions { - return this._options; - } - - set options(value: vscode.TextEditorOptions) { - if (!this._disposed) { - this._options.assign(value); - } - } + // --- incoming: extension host MUST accept what the renderer says _acceptOptions(options: IResolvedTextEditorConfiguration): void { ok(!this._disposed); this._options._accept(options); } - // ---- visible ranges - - get visibleRanges(): Range[] { - return this._visibleRanges; - } - - set visibleRanges(value: Range[]) { - throw readonly('visibleRanges'); - } - _acceptVisibleRanges(value: Range[]): void { ok(!this._disposed); this._visibleRanges = value; } - // ---- view column - - get viewColumn(): vscode.ViewColumn | undefined { - return this._viewColumn; - } - - set viewColumn(value) { - throw readonly('viewColumn'); - } - _acceptViewColumn(value: vscode.ViewColumn) { ok(!this._disposed); this._viewColumn = value; } - // ---- selections - - get selection(): Selection { - return this._selections && this._selections[0]; - } - - set selection(value: Selection) { - if (!(value instanceof Selection)) { - throw illegalArgument('selection'); - } - this._selections = [value]; - this._trySetSelection(); - } - - get selections(): Selection[] { - return this._selections; - } - - set selections(value: Selection[]) { - if (!Array.isArray(value) || value.some(a => !(a instanceof Selection))) { - throw illegalArgument('selections'); - } - this._selections = value; - this._trySetSelection(); - } - - setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { - const willBeEmpty = (ranges.length === 0); - if (willBeEmpty && !this._hasDecorationsForKey[decorationType.key]) { - // avoid no-op call to the renderer - return; - } - if (willBeEmpty) { - delete this._hasDecorationsForKey[decorationType.key]; - } else { - this._hasDecorationsForKey[decorationType.key] = true; - } - this._runOnProxy( - () => { - if (TypeConverters.isDecorationOptionsArr(ranges)) { - return this._proxy.$trySetDecorations( - this.id, - decorationType.key, - TypeConverters.fromRangeOrRangeWithMessage(ranges) - ); - } else { - const _ranges: number[] = new Array(4 * ranges.length); - for (let i = 0, len = ranges.length; i < len; i++) { - const range = ranges[i]; - _ranges[4 * i] = range.start.line + 1; - _ranges[4 * i + 1] = range.start.character + 1; - _ranges[4 * i + 2] = range.end.line + 1; - _ranges[4 * i + 3] = range.end.character + 1; - } - return this._proxy.$trySetDecorationsFast( - this.id, - decorationType.key, - _ranges - ); - } - } - ); - } - - revealRange(range: Range, revealType: vscode.TextEditorRevealType): void { - this._runOnProxy( - () => this._proxy.$tryRevealRange( - this.id, - TypeConverters.Range.from(range), - (revealType || TextEditorRevealType.Default) - ) - ); - } - - private _trySetSelection(): Promise { - const selection = this._selections.map(TypeConverters.Selection.from); - return this._runOnProxy(() => this._proxy.$trySetSelections(this.id, selection)); - } - _acceptSelections(selections: Selection[]): void { ok(!this._disposed); this._selections = selections; } - // ---- editing - - edit(callback: (edit: TextEditorEdit) => void, options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Promise { - if (this._disposed) { - return Promise.reject(new Error('TextEditor#edit not possible on closed editors')); - } - const edit = new TextEditorEdit(this._documentData.document, options); - callback(edit); - return this._applyEdit(edit); + private async _trySetSelection(): Promise { + const selection = this._selections.map(TypeConverters.Selection.from); + await this._runOnProxy(() => this._proxy.$trySetSelections(this.id, selection)); + return this.value; } private _applyEdit(editBuilder: TextEditorEdit): Promise { @@ -613,44 +601,12 @@ export class ExtHostTextEditor implements vscode.TextEditor { undoStopAfter: editData.undoStopAfter }); } - - insertSnippet(snippet: SnippetString, where?: Position | readonly Position[] | Range | readonly Range[], options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Promise { - if (this._disposed) { - return Promise.reject(new Error('TextEditor#insertSnippet not possible on closed editors')); - } - let ranges: IRange[]; - - if (!where || (Array.isArray(where) && where.length === 0)) { - ranges = this._selections.map(range => TypeConverters.Range.from(range)); - - } else if (where instanceof Position) { - const { lineNumber, column } = TypeConverters.Position.from(where); - ranges = [{ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column }]; - - } else if (where instanceof Range) { - ranges = [TypeConverters.Range.from(where)]; - } else { - ranges = []; - for (const posOrRange of where) { - if (posOrRange instanceof Range) { - ranges.push(TypeConverters.Range.from(posOrRange)); - } else { - const { lineNumber, column } = TypeConverters.Position.from(posOrRange); - ranges.push({ startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: column }); - } - } - } - - return this._proxy.$tryInsertSnippet(this.id, snippet.value, ranges, options); - } - - // ---- util - private _runOnProxy(callback: () => Promise): Promise { if (this._disposed) { this._logService.warn('TextEditor is closed/disposed'); return Promise.resolve(undefined); } + return callback().then(() => this, err => { if (!(err instanceof Error && err.name === 'DISPOSED')) { this._logService.warn(err); @@ -659,4 +615,3 @@ export class ExtHostTextEditor implements vscode.TextEditor { }); } } - diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index a6777aa213..b4ff85384b 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -41,12 +41,17 @@ export class ExtHostEditors implements ExtHostEditorsShape { this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); } - getActiveTextEditor(): ExtHostTextEditor | undefined { + getActiveTextEditor(): vscode.TextEditor | undefined { return this._extHostDocumentsAndEditors.activeEditor(); } - getVisibleTextEditors(): vscode.TextEditor[] { - return this._extHostDocumentsAndEditors.allEditors(); + getVisibleTextEditors(): vscode.TextEditor[]; + getVisibleTextEditors(internal: true): ExtHostTextEditor[]; + getVisibleTextEditors(internal?: true): ExtHostTextEditor[] | vscode.TextEditor[] { + const editors = this._extHostDocumentsAndEditors.allEditors(); + return internal + ? editors + : editors.map(editor => editor.value); } showTextDocument(document: vscode.TextDocument, column: vscode.ViewColumn, preserveFocus: boolean): Promise; @@ -75,7 +80,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { const editorId = await this._proxy.$tryShowTextDocument(document.uri, options); const editor = editorId && this._extHostDocumentsAndEditors.getEditor(editorId); if (editor) { - return editor; + return editor.value; } // we have no editor... having an id means that we had an editor // on the main side and that it isn't the current editor anymore... @@ -87,7 +92,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { } createTextEditorDecorationType(options: vscode.DecorationRenderOptions): vscode.TextEditorDecorationType { - return new TextEditorDecorationType(this._proxy, options); + return new TextEditorDecorationType(this._proxy, options).value; } // --- called from main thread @@ -114,7 +119,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { // (2) fire change events if (data.options) { this._onDidChangeTextEditorOptions.fire({ - textEditor: textEditor, + textEditor: textEditor.value, options: { ...data.options, lineNumbers: TypeConverters.TextEditorLineNumbersStyle.to(data.options.lineNumbers) } }); } @@ -122,7 +127,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { const kind = TextEditorSelectionChangeKind.fromValue(data.selections.source); const selections = data.selections.selections.map(TypeConverters.Selection.to); this._onDidChangeTextEditorSelection.fire({ - textEditor, + textEditor: textEditor.value, selections, kind }); @@ -130,7 +135,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { if (data.visibleRanges) { const visibleRanges = arrays.coalesce(data.visibleRanges.map(TypeConverters.Range.to)); this._onDidChangeTextEditorVisibleRanges.fire({ - textEditor, + textEditor: textEditor.value, visibleRanges }); } @@ -143,9 +148,9 @@ export class ExtHostEditors implements ExtHostEditorsShape { throw new Error('Unknown text editor'); } const viewColumn = TypeConverters.ViewColumn.to(data[id]); - if (textEditor.viewColumn !== viewColumn) { + if (textEditor.value.viewColumn !== viewColumn) { textEditor._acceptViewColumn(viewColumn); - this._onDidChangeTextEditorViewColumn.fire({ textEditor, viewColumn }); + this._onDidChangeTextEditorViewColumn.fire({ textEditor: textEditor.value, viewColumn }); } } } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 76ea2283c2..79f1be45f8 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -21,6 +21,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Command } from 'vs/editor/common/modes'; // {{SQL CARBON EDIT}} import * as azdata from 'azdata'; @@ -134,12 +135,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.hasResolve; } - $resolve(treeViewId: string, treeItemHandle: string): Promise { + $resolve(treeViewId: string, treeItemHandle: string, token: vscode.CancellationToken): Promise { const treeView = this.treeViews.get(treeViewId); if (!treeView) { throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId)); } - return treeView.resolveTreeItem(treeItemHandle); + return treeView.resolveTreeItem(treeItemHandle, token); } $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void { @@ -186,6 +187,7 @@ export interface TreeNode extends IDisposable { // {{SQL CARBON EDIT}} export in extensionItem: vscode.TreeItem; parent: TreeNode | Root; children?: TreeNode[]; + disposableStore: DisposableStore; } // {{SQL CARBON EDIT}} @@ -377,7 +379,7 @@ export class ExtHostTreeView extends Disposable { return !!this.dataProvider.resolveTreeItem; } - async resolveTreeItem(treeItemHandle: string): Promise { + async resolveTreeItem(treeItemHandle: string, token: vscode.CancellationToken): Promise { if (!this.dataProvider.resolveTreeItem) { return undefined; // {{SQL CARBON EDIT}} strict-null-checks } @@ -385,9 +387,10 @@ export class ExtHostTreeView extends Disposable { if (element) { const node = this.nodes.get(element); if (node) { - const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element) ?? node.extensionItem; - // Resolvable elements. Currently only tooltip. + const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element, token) ?? node.extensionItem; + // Resolvable elements. Currently only tooltip and command. node.item.tooltip = this.getTooltip(resolve.tooltip); + node.item.command = this.getCommand(node.disposableStore, resolve.command); return node.item; } } @@ -584,8 +587,12 @@ export class ExtHostTreeView extends Disposable { return tooltip; } + private getCommand(disposable: DisposableStore, command?: vscode.Command): Command | undefined { + return command ? this.commands.toInternal(command, disposable) : undefined; + } + protected createTreeNode(element: T, extensionTreeItem: azdata.TreeItem, parent: TreeNode | Root): TreeNode { // {{SQL CARBON EDIT}} change to protected, change to azdata.TreeItem - const disposable = new DisposableStore(); + const disposableStore = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); // {{SQL CARBON EDIT}} @@ -596,7 +603,7 @@ export class ExtHostTreeView extends Disposable { description: extensionTreeItem.description, resourceUri: extensionTreeItem.resourceUri, tooltip: this.getTooltip(extensionTreeItem.tooltip), - command: extensionTreeItem.command ? this.commands.toInternal(extensionTreeItem.command, disposable) : undefined, + command: this.getCommand(disposableStore, extensionTreeItem.command), contextValue: extensionTreeItem.contextValue, icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, @@ -613,7 +620,8 @@ export class ExtHostTreeView extends Disposable { extensionItem: extensionTreeItem, parent, children: undefined, - dispose(): void { disposable.dispose(); } + disposableStore, + dispose(): void { disposableStore.dispose(); } }; } diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 510204a39f..5e33086380 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtHostTunnelServiceShape, MainContext, MainThreadTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; import { RemoteTunnel, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; @@ -11,18 +11,27 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; export interface TunnelDto { remoteAddress: { port: number, host: string }; localAddress: { port: number, host: string } | string; + public: boolean; } export namespace TunnelDto { export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto { - return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress }; + return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress, public: !!tunnel.public }; } export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto { - return { remoteAddress: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + return { + remoteAddress: { + host: tunnel.tunnelRemoteHost, + port: tunnel.tunnelRemotePort + }, + localAddress: tunnel.localAddress, + public: tunnel.public + }; } } @@ -44,12 +53,13 @@ export const IExtHostTunnelService = createDecorator('IEx export class ExtHostTunnelService implements IExtHostTunnelService { declare readonly _serviceBrand: undefined; onDidChangeTunnels: vscode.Event = (new Emitter()).event; - private readonly _proxy: MainThreadTunnelServiceShape; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, ) { - this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); + } + async $applyCandidateFilter(candidates: CandidatePort[]): Promise { + return candidates; } async openTunnel(extension: IExtensionDescription, forward: TunnelOptions): Promise { @@ -59,10 +69,10 @@ export class ExtHostTunnelService implements IExtHostTunnelService { return []; } async setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise { - await this._proxy.$tunnelServiceReady(); return { dispose: () => { } }; } - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined { return undefined; } + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { return undefined; } async $closeTunnel(remote: { host: string, port: number }): Promise { } async $onDidTunnelsChange(): Promise { } + async $registerCandidateFinder(): Promise { } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ef18a8d506..f207489053 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1015,6 +1015,29 @@ export namespace SignatureHelp { } } +export namespace InlineHint { + + export function from(hint: vscode.InlineHint): modes.InlineHint { + return { + text: hint.text, + range: Range.from(hint.range), + description: hint.description && MarkdownString.fromStrict(hint.description), + whitespaceBefore: hint.whitespaceBefore, + whitespaceAfter: hint.whitespaceAfter + }; + } + + export function to(hint: modes.InlineHint): vscode.InlineHint { + return new types.InlineHint( + hint.text, + Range.to(hint.range), + htmlContent.isMarkdownString(hint.description) ? MarkdownString.to(hint.description) : hint.description, + hint.whitespaceBefore, + hint.whitespaceAfter + ); + } +} + export namespace DocumentLink { export function from(link: vscode.DocumentLink): modes.ILink { @@ -1389,19 +1412,21 @@ export namespace TestState { export namespace TestItem { - export function from(item: vscode.TestItem): ITestItem { + export function from(item: vscode.TestItem, parentExtId?: string): ITestItem { return { + extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label), label: item.label, location: item.location ? location.from(item.location) : undefined, - debuggable: item.debuggable, + debuggable: item.debuggable ?? false, description: item.description, - runnable: item.runnable, + runnable: item.runnable ?? true, state: TestState.from(item.state), }; } - export function to(item: ITestItem): vscode.TestItem { + export function toShallow(item: ITestItem): Omit { return { + id: item.extId, label: item.label, location: item.location && location.to({ range: item.location.range, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1d205603f4..7bd3321340 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, ICellEditOperation, ICellOutputEdit, ITransformedDisplayOutputDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; function es5ClassCompat(target: Function): any { @@ -638,7 +638,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // --- notebook replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: FileEditType.Cell, metadata, uri, notebookMetadata: value }); + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: { ...notebookDocumentMetadataDefaults, ...value } }, notebookMetadata: value }); } replaceNotebookCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -648,17 +648,27 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } replaceNotebookCellOutput(uri: URI, index: number, outputs: (vscode.NotebookCellOutput | vscode.CellOutput)[], metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ - _type: FileEditType.Cell, metadata, uri, edit: { - editType: CellEditType.Output, index, outputs: outputs.map(output => { - if (NotebookCellOutput.isNotebookCellOutput(output)) { - return addIdToOutput(output.toJSON()); - } else { - return addIdToOutput(output); - } - }) - } - }); + this._editNotebookCellOutput(uri, index, false, outputs, metadata); + } + + appendNotebookCellOutput(uri: URI, index: number, outputs: (vscode.NotebookCellOutput | vscode.CellOutput)[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._editNotebookCellOutput(uri, index, true, outputs, metadata); + } + + private _editNotebookCellOutput(uri: URI, index: number, append: boolean, outputs: (vscode.NotebookCellOutput | vscode.CellOutput)[], metadata: vscode.WorkspaceEditEntryMetadata | undefined): void { + const edit: ICellOutputEdit = { + editType: CellEditType.Output, + index, + append, + outputs: outputs.map(output => { + if (NotebookCellOutput.isNotebookCellOutput(output)) { + return output.toJSON(); + } else { + return addIdToOutput(output); + } + }) + }; + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit }); } replaceNotebookCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -1373,6 +1383,23 @@ export enum SignatureHelpTriggerKind { ContentChange = 3, } +@es5ClassCompat +export class InlineHint { + text: string; + range: Range; + description?: string | vscode.MarkdownString; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + + constructor(text: string, range: Range, description?: string | vscode.MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean) { + this.text = text; + this.range = range; + this.description = description; + this.whitespaceBefore = whitespaceBefore; + this.whitespaceAfter = whitespaceAfter; + } +} + export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, @@ -2298,9 +2325,6 @@ export class FunctionBreakpoint extends Breakpoint { constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { super(enabled, condition, hitCondition, logMessage); - if (!functionName) { - throw illegalArgument('functionName'); - } this.functionName = functionName; } } @@ -2802,12 +2826,11 @@ export class NotebookCellOutput { return obj instanceof NotebookCellOutput; } - constructor( - readonly outputs: NotebookCellOutputItem[], - readonly metadata?: Record - ) { } + readonly id: string = generateUuid(); - toJSON(): IDisplayOutput { + constructor(readonly outputs: NotebookCellOutputItem[]) { } + + toJSON(): ITransformedDisplayOutputDto { let data: { [key: string]: unknown; } = {}; let custom: { [key: string]: unknown; } = {}; let hasMetadata = false; @@ -2820,6 +2843,7 @@ export class NotebookCellOutput { } } return { + outputId: this.id, outputKind: CellOutputKind.Rich, data, metadata: hasMetadata ? { custom } : undefined @@ -2858,7 +2882,8 @@ export enum NotebookCellStatusBarAlignment { export enum NotebookEditorRevealType { Default = 0, InCenter = 1, - InCenterIfOutsideViewport = 2 + InCenterIfOutsideViewport = 2, + AtTop = 3 } @@ -2924,11 +2949,12 @@ export class LinkedEditingRanges { //#region Testing export enum TestRunState { Unset = 0, - Running = 1, - Passed = 2, - Failed = 3, - Skipped = 4, - Errored = 5 + Queued = 1, + Running = 2, + Passed = 3, + Failed = 4, + Skipped = 5, + Errored = 6 } export enum TestMessageSeverity { @@ -2963,15 +2989,15 @@ export class TestState { } } -type AllowedUndefined = 'description' | 'location'; - -/** - * Test item without any optional properties. Only some properties are - * permitted to be undefined, but they must still exist. - */ -export type RequiredTestItem = { - [K in keyof Required]: K extends AllowedUndefined ? vscode.TestItem[K] : Required[K] -}; +export type RequiredTestItem = vscode.RequiredTestItem; +export type TestItem = vscode.TestItem; //#endregion + +export enum ExternalUriOpenerPriority { + None = 0, + Option = 1, + Default = 2, + Preferred = 3, +} diff --git a/src/vs/workbench/api/common/extHostUriOpener.ts b/src/vs/workbench/api/common/extHostUriOpener.ts new file mode 100644 index 0000000000..9eae665d4f --- /dev/null +++ b/src/vs/workbench/api/common/extHostUriOpener.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import type * as vscode from 'vscode'; +import { ExtHostUriOpenersShape, IMainContext, MainContext, MainThreadUriOpenersShape } from './extHost.protocol'; + + +export class ExtHostUriOpeners implements ExtHostUriOpenersShape { + + private static readonly supportedSchemes = new Set([Schemas.http, Schemas.https]); + + private readonly _proxy: MainThreadUriOpenersShape; + + private readonly _openers = new Map(); + + constructor( + mainContext: IMainContext, + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadUriOpeners); + } + + registerExternalUriOpener( + extensionId: ExtensionIdentifier, + id: string, + opener: vscode.ExternalUriOpener, + metadata: vscode.ExternalUriOpenerMetadata, + ): vscode.Disposable { + if (this._openers.has(id)) { + throw new Error(`Opener with id '${id}' already registered`); + } + + const invalidScheme = metadata.schemes.find(scheme => !ExtHostUriOpeners.supportedSchemes.has(scheme)); + if (invalidScheme) { + throw new Error(`Scheme '${invalidScheme}' is not supported. Only http and https are currently supported.`); + } + + this._openers.set(id, opener); + this._proxy.$registerUriOpener(id, metadata.schemes, extensionId, metadata.label); + + return toDisposable(() => { + this._openers.delete(id); + this._proxy.$unregisterUriOpener(id); + }); + } + + async $canOpenUri(id: string, uriComponents: UriComponents, token: CancellationToken): Promise { + const opener = this._openers.get(id); + if (!opener) { + throw new Error(`Unknown opener with id: ${id}`); + } + + const uri = URI.revive(uriComponents); + return opener.canOpenExternalUri(uri, token); + } + + async $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise { + const opener = this._openers.get(id); + if (!opener) { + throw new Error(`Unknown opener id: '${id}'`); + } + + return opener.openExternalUri(URI.revive(context.resolvedUri), { + sourceUri: URI.revive(context.sourceUri) + }, token); + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index 39ee8ec136..bede83233b 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -287,8 +287,8 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel await serializer.deserializeWebviewPanel(revivedPanel, state); } - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, position, options, webview); this._webviewPanels.set(webviewHandle, panel); return panel; } diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 31b475d430..1fa79ec7b9 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { Counter } from 'vs/base/common/numbers'; -import { basename, basenameOrAuthority, dirname, isEqual, relativePath } from 'vs/base/common/resources'; +import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from 'vs/base/common/resources'; import { compare } from 'vs/base/common/strings'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -37,27 +37,27 @@ export interface IExtHostWorkspaceProvider { resolveProxy(url: string): Promise; } -function isFolderEqual(folderA: URI, folderB: URI): boolean { - return isEqual(folderA, folderB); +function isFolderEqual(folderA: URI, folderB: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean { + return new ExtUri(uri => ignorePathCasing(uri, extHostFileSystemInfo)).isEqual(folderA, folderB); } -function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { - return isFolderEqual(a.uri, b.uri) ? 0 : compare(a.uri.toString(), b.uri.toString()); +function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number { + return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? 0 : compare(a.uri.toString(), b.uri.toString()); } -function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { +function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number { if (a.index !== b.index) { return a.index < b.index ? -1 : 1; } - return isFolderEqual(a.uri, b.uri) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); + return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); } -function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder) => number): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { - const oldSortedFolders = oldFolders.slice(0).sort(compare); - const newSortedFolders = newFolders.slice(0).sort(compare); +function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo) => number, extHostFileSystemInfo: IExtHostFileSystemInfo): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { + const oldSortedFolders = oldFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo)); + const newSortedFolders = newFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo)); - return arrayDelta(oldSortedFolders, newSortedFolders, compare); + return arrayDelta(oldSortedFolders, newSortedFolders, (a, b) => compare(a, b, extHostFileSystemInfo)); } function ignorePathCasing(uri: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean { @@ -87,7 +87,7 @@ class ExtHostWorkspaceImpl extends Workspace { if (previousConfirmedWorkspace) { folders.forEach((folderData, index) => { const folderUri = URI.revive(folderData.uri); - const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri); + const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri, extHostFileSystemInfo); if (existingFolder) { existingFolder.name = folderData.name; @@ -106,15 +106,15 @@ class ExtHostWorkspaceImpl extends Workspace { newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1); const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders, configuration ? URI.revive(configuration) : null, !!isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo)); - const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri); + const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri, extHostFileSystemInfo); return { workspace, added, removed }; } - private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI): MutableWorkspaceFolder | undefined { + private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): MutableWorkspaceFolder | undefined { for (let i = 0; i < workspace.folders.length; i++) { const folder = workspace.workspaceFolders[i]; - if (isFolderEqual(folder.uri, folderUriToFind)) { + if (isFolderEqual(folder.uri, folderUriToFind, extHostFileSystemInfo)) { return folder; } } @@ -254,7 +254,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[] = []; if (Array.isArray(workspaceFoldersToAdd)) { workspaceFoldersToAdd.forEach(folderToAdd => { - if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri))) { + if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) { validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) }); } }); @@ -283,13 +283,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac for (let i = 0; i < newWorkspaceFolders.length; i++) { const folder = newWorkspaceFolders[i]; - if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri))) { + if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri, this._extHostFileSystemInfo))) { return false; // cannot add the same folder multiple times } } newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index - const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex); + const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex, this._extHostFileSystemInfo); if (added.length === 0 && removed.length === 0) { return false; // nothing actually changed } diff --git a/src/vs/workbench/api/common/shared/workspaceContains.ts b/src/vs/workbench/api/common/shared/workspaceContains.ts index c55fafc6be..e5a250f366 100644 --- a/src/vs/workbench/api/common/shared/workspaceContains.ts +++ b/src/vs/workbench/api/common/shared/workspaceContains.ts @@ -119,8 +119,7 @@ export function checkGlobFileExists( const queryBuilder = instantiationService.createInstance(QueryBuilder); const query = queryBuilder.file(folders.map(folder => toWorkspaceFolder(URI.revive(folder))), { _reason: 'checkExists', - includePattern: includes.join(', '), - expandPatterns: true, + includePattern: includes, exists: true }); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index eed4c05678..7ac96ace14 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -34,6 +34,14 @@ export interface StatusPipeArgs { } +export interface ExtensionManagementPipeArgs { + type: 'extensionManagement'; + list?: { showVersions?: boolean, category?: string; }; + install?: string[]; + uninstall?: string[]; + force?: boolean; +} + export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | OpenExternalCommandPipeArgs; export interface ICommandsExecuter { @@ -86,6 +94,10 @@ export class CLIServerBase { case 'status': this.getStatus(data, res); break; + case 'extensionManagement': + this.manageExtensions(data, res) + .catch(this.logService.error); + break; default: res.writeHead(404); res.write(`Unknown message type: ${data.type}`, err => { @@ -134,14 +146,34 @@ export class CLIServerBase { res.end(); } - private openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { + private async openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { for (const uri of data.uris) { - this._commands.executeCommand('_workbench.openExternal', URI.parse(uri), { allowTunneling: true }); + await this._commands.executeCommand('_remoteCLI.openExternal', URI.parse(uri), { allowTunneling: true }); } res.writeHead(200); res.end(); } + private async manageExtensions(data: ExtensionManagementPipeArgs, res: http.ServerResponse) { + console.log('server: manageExtensions'); + try { + const toExtOrVSIX = (inputs: string[] | undefined) => inputs?.map(input => /\.vsix$/i.test(input) ? URI.parse(input) : input); + const commandArgs = { + list: data.list, + install: toExtOrVSIX(data.install), + uninstall: toExtOrVSIX(data.uninstall), + force: data.force + }; + const output = await this._commands.executeCommand('_remoteCLI.manageExtensions', commandArgs, { allowTunneling: true }); + res.writeHead(200); + res.write(output); + } catch (e) { + res.writeHead(500); + res.write(String(e)); + } + res.end(); + } + private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) { try { const status = await this._commands.executeCommand('_remoteCLI.getSystemStatus'); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 2ff01c4b2d..2ea578c40a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -25,7 +25,6 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; - export class ExtHostDebugService extends ExtHostDebugServiceBase { readonly _serviceBrand: undefined; @@ -68,7 +67,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { return new SignService(); } - public async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise { + public async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise { if (args.kind === 'integrated') { @@ -104,7 +103,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { cwdForPrepareCommand = args.cwd; } - terminal.show(); + terminal.show(true); const shellProcessId = await terminal.processId; @@ -116,13 +115,21 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { const command = prepareCommand(shell, args.args, cwdForPrepareCommand, args.env); terminal.sendText(command, true); + // Mark terminal as unused when its session ends, see #112055 + const sessionListener = this.onDidTerminateDebugSession(s => { + if (s.id === sessionId) { + this._integratedTerminalInstances.free(terminal!); + sessionListener.dispose(); + } + }); + return shellProcessId; } else if (args.kind === 'external') { return runInExternalTerminal(args, await this._configurationService.getConfigProvider()); } - return super.$runInTerminal(args); + return super.$runInTerminal(args, sessionId); } protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { @@ -139,17 +146,15 @@ class DebugTerminalCollection { private _terminalInstances = new Map(); public async checkout(config: string) { - const entries = [...this._terminalInstances.keys()]; - const promises = entries.map((terminal) => createCancelablePromise(async ct => { - const pid = await terminal.processId; - if (await hasChildProcesses(pid)) { + const entries = [...this._terminalInstances.entries()]; + const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => { + if (termInfo.lastUsedAt !== -1 && await hasChildProcesses(await terminal.processId)) { return null; } // important: date check and map operations must be synchronous const now = Date.now(); - const termInfo = this._terminalInstances.get(terminal); - if (!termInfo || termInfo.lastUsedAt + DebugTerminalCollection.minUseDelay > now || ct.isCancellationRequested) { + if (termInfo.lastUsedAt + DebugTerminalCollection.minUseDelay > now || ct.isCancellationRequested) { return null; } @@ -168,6 +173,13 @@ class DebugTerminalCollection { this._terminalInstances.set(terminal, { lastUsedAt: Date.now(), config: termConfig }); } + public free(terminal: vscode.Terminal) { + const info = this._terminalInstances.get(terminal); + if (info) { + info.lastUsedAt = -1; + } + } + public onTerminalClosed(terminal: vscode.Terminal) { this._terminalInstances.delete(terminal); } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 470d60fb6f..354371df40 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createApiFactoryAndRegisterActors } from 'sql/workbench/api/common/sqlExtHost.api.impl'; // {{SQL CARBON EDIT}} replace with ours +import * as performance from 'vs/base/common/performance'; +import { createApiFactoryAndRegisterActors } from 'sql/workbench/api/common/sqlExtHost.api.impl'; // {{SQL CARBON EDIT}} replace with ours import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator'; @@ -13,7 +14,7 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -62,10 +63,12 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Module loading tricks const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); await interceptor.install(); + performance.mark('code/extHost/didInitAPI'); // Do this when extension service exists, but extensions are not being activated yet. const configProvider = await this._extHostConfiguration.getConfigProvider(); await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData); + performance.mark('code/extHost/didInitProxyResolver'); // Use IPC messages to forward console-calls, note that the console is // already patched to use`process.send()` @@ -84,7 +87,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.main; } - protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { if (module.scheme !== Schemas.file) { throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); } @@ -93,10 +96,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`); this._logService.flush(); try { + if (extensionId) { + performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`); + } r = require.__$__nodeRequire(module.fsPath); } catch (e) { return Promise.reject(e); } finally { + if (extensionId) { + performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`); + } activationTimesBuilder.codeLoadingStop(); } return Promise.resolve(r); diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 2c9fbdc146..29f831d5a4 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -9,7 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; import { toLocalISOString } from 'vs/base/common/date'; -import { dirExists, mkdirp } from 'vs/base/node/pfs'; +import { SymlinkSupport } from 'vs/base/node/pfs'; +import { promises } from 'fs'; import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -85,9 +86,9 @@ export class ExtHostOutputService2 extends ExtHostOutputService { private async _doCreateOutChannel(name: string): Promise { try { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); - const exists = await dirExists(outputDirPath); + const exists = await SymlinkSupport.existsDirectory(outputDirPath); if (!exists) { - await mkdirp(outputDirPath); + await promises.mkdir(outputDirPath, { recursive: true }); } const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`; const file = URI.file(join(outputDirPath, `${fileName}.log`)); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 7a8a317757..f6f28906ec 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -48,11 +48,12 @@ export class ExtHostTask extends ExtHostTaskBase { } public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise { - if (!task.execution) { + const tTask = (task as types.Task); + + if (!task.execution && (tTask._id === undefined)) { throw new Error('Tasks to execute must include an execution'); } - const tTask = (task as types.Task); // We have a preserved ID. So the task didn't change. if (tTask._id !== undefined) { // Always get the task execution first to prevent timing issues when retrieving it later diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index e4ee2e2980..5e4ae5cd27 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -17,13 +17,15 @@ import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/ext import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { getSystemShell, detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal'; +import { detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal'; import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { getSystemShell, getSystemShellSync } from 'vs/base/node/shell'; +import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostTerminalService extends BaseExtHostTerminalService { @@ -32,6 +34,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { // TODO: Pull this from main side private _isWorkspaceShellAllowed: boolean = false; + private _defaultShell: string | undefined; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -42,20 +45,26 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { @IExtHostInitDataService private _extHostInitDataService: IExtHostInitDataService ) { super(true, extHostRpc); + + // Getting the SystemShell is an async operation, however, the ExtHost terminal service is mostly synchronous + // and the API `vscode.env.shell` is also synchronous. The default shell _should_ be set when extensions are + // starting up but if not, we run getSystemShellSync below which gets a sane default. + getSystemShell(platform.platform).then(s => this._defaultShell = s); + this._updateLastActiveWorkspace(); this._updateVariableResolver(); this._registerListeners(); } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, { name, shellPath, shellArgs }, name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name); this._terminals.push(terminal); terminal.create(shellPath, shellArgs); - return terminal; + return terminal.value; } public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal { - const terminal = new ExtHostTerminal(this._proxy, options, options.name); + const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); this._terminals.push(terminal); terminal.create( withNullAsUndefined(options.shellPath), @@ -66,7 +75,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { withNullAsUndefined(options.strictEnv), withNullAsUndefined(options.hideFromUser), withNullAsUndefined(isFeatureTerminal)); - return terminal; + return terminal.value; } public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { @@ -76,10 +85,11 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { .inspect(key.substr(key.lastIndexOf('.') + 1)); return this._apiInspectConfigToPlain(setting); }; + return terminalEnvironment.getDefaultShell( fetchSetting, this._isWorkspaceShellAllowed, - getSystemShell(platform.platform), + this._defaultShell ?? getSystemShellSync(platform.platform), process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), process.env.windir, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), @@ -139,7 +149,8 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { executable: shellLaunchConfigDto.executable, args: shellLaunchConfigDto.args, cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), - env: shellLaunchConfigDto.env + env: shellLaunchConfigDto.env, + flowControl: shellLaunchConfigDto.flowControl }; // Merge in shell and args from settings diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index f19bb88782..43a348dfff 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -12,12 +12,15 @@ import { URI } from 'vs/base/common/uri'; import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; import * as fs from 'fs'; +import * as pfs from 'vs/base/node/pfs'; import { isLinux } from 'vs/base/common/platform'; import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { asPromise } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { MovingAverage } from 'vs/base/common/numbers'; +import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ILogService } from 'vs/platform/log/common/log'; class ExtensionTunnel implements vscode.Tunnel { private _onDispose: Emitter = new Emitter(); @@ -26,14 +29,106 @@ class ExtensionTunnel implements vscode.Tunnel { constructor( public readonly remoteAddress: { port: number, host: string }, public readonly localAddress: { port: number, host: string } | string, - private readonly _dispose: () => void) { } + private readonly _dispose: () => Promise) { } - dispose(): void { + dispose(): Promise { this._onDispose.fire(); - this._dispose(); + return this._dispose(); } } +export function getSockets(stdout: string): { pid: number, socket: number }[] { + const lines = stdout.trim().split('\n'); + const mapped: { pid: number, socket: number }[] = []; + lines.forEach(line => { + const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; + if (match && match.length >= 3) { + mapped.push({ + pid: parseInt(match[1], 10), + socket: parseInt(match[2], 10) + }); + } + }); + return mapped; +} + +export function loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { + const table = ([] as Record[]).concat(...stdouts.map(loadConnectionTable)); + return [ + ...new Map( + table.filter(row => row.st === '0A') + .map(row => { + const address = row.local_address.split(':'); + return { + socket: parseInt(row.inode, 10), + ip: parseIpAddress(address[0]), + port: parseInt(address[1], 16) + }; + }).map(port => [port.ip + ':' + port.port, port]) + ).values() + ]; +} + +export function parseIpAddress(hex: string): string { + let result = ''; + if (hex.length === 8) { + for (let i = hex.length - 2; i >= 0; i -= 2) { + result += parseInt(hex.substr(i, 2), 16); + if (i !== 0) { + result += '.'; + } + } + } else { + for (let i = hex.length - 4; i >= 0; i -= 4) { + result += parseInt(hex.substr(i, 4), 16).toString(16); + if (i !== 0) { + result += ':'; + } + } + } + return result; +} + +export function loadConnectionTable(stdout: string): Record[] { + const lines = stdout.trim().split('\n'); + const names = lines.shift()!.trim().split(/\s+/) + .filter(name => name !== 'rx_queue' && name !== 'tm->when'); + const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { + obj[names[i] || i] = value; + return obj; + }, {} as Record)); + return table; +} + +function knownExcludeCmdline(command: string): boolean { + return !!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) + || (command.indexOf('out/vs/server/main.js') !== -1) + || (command.indexOf('_productName=VSCode') !== -1); +} + +export async function findPorts(tcp: string, tcp6: string, procSockets: string, processes: { pid: number, cwd: string, cmd: string }[]): Promise { + const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6); + const sockets = getSockets(procSockets); + + const socketMap = sockets.reduce((m, socket) => { + m[socket.socket] = socket; + return m; + }, {} as Record); + const processMap = processes.reduce((m, process) => { + m[process.pid] = process; + return m; + }, {} as Record); + + const ports: CandidatePort[] = []; + connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { + const command = processMap[socketMap[socket].pid].cmd; + if (!knownExcludeCmdline(command)) { + ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd, pid: socketMap[socket].pid }); + } + }); + return ports; +} + export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadTunnelServiceShape; @@ -42,15 +137,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe private _extensionTunnels: Map> = new Map(); private _onDidChangeTunnels: Emitter = new Emitter(); onDidChangeTunnels: vscode.Event = this._onDidChangeTunnels.event; + private _candidateFindingEnabled: boolean = false; constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostInitDataService initData: IExtHostInitDataService + @IExtHostInitDataService initData: IExtHostInitDataService, + @ILogService private readonly logService: ILogService ) { super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService); - if (initData.remote.isRemote && initData.remote.authority) { - this.registerCandidateFinder(); + if (isLinux && initData.remote.isRemote && initData.remote.authority) { + this._proxy.$setCandidateFinder(); } } @@ -70,18 +167,30 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return this._proxy.$getTunnels(); } - registerCandidateFinder(): void { - // Every two seconds, scan to see if the candidate ports have changed; - if (isLinux) { - let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined; - setInterval(async () => { - const newPorts = await this.findCandidatePorts(); - if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) { - oldPorts = newPorts; - this._proxy.$onFoundNewCandidates(oldPorts.filter(async (candidate) => await this._showCandidatePort(candidate.host, candidate.port, candidate.detail))); - return; - } - }, 2000); + private calculateDelay(movingAverage: number) { + // Some local testing indicated that the moving average might be between 50-100 ms. + return Math.max(movingAverage * 20, 2000); + } + + async $registerCandidateFinder(enable: boolean): Promise { + if (enable && this._candidateFindingEnabled) { + // already enabled + return; + } + this._candidateFindingEnabled = enable; + // Regularly scan to see if the candidate ports have changed. + let movingAverage = new MovingAverage(); + let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined; + while (this._candidateFindingEnabled) { + const startTime = new Date().getTime(); + const newPorts = await this.findCandidatePorts(); + const timeTaken = new Date().getTime() - startTime; + movingAverage.update(timeTaken); + if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) { + oldPorts = newPorts; + await this._proxy.$onFoundNewCandidates(oldPorts); + } + await (new Promise(resolve => setTimeout(() => resolve(), this.calculateDelay(movingAverage.value)))); } } @@ -89,15 +198,18 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (provider) { if (provider.showCandidatePort) { this._showCandidatePort = provider.showCandidatePort; + await this._proxy.$setCandidateFilter(); } if (provider.tunnelFactory) { this._forwardPortProvider = provider.tunnelFactory; - await this._proxy.$setTunnelProvider(); + await this._proxy.$setTunnelProvider(provider.tunnelFeatures ?? { + elevation: false, + public: false + }); } } else { this._forwardPortProvider = undefined; } - await this._proxy.$tunnelServiceReady(); return toDisposable(() => { this._forwardPortProvider = undefined; }); @@ -110,7 +222,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (silent) { hostMap.get(remote.port)!.disposeListener.dispose(); } - hostMap.get(remote.port)!.tunnel.dispose(); + await hostMap.get(remote.port)!.tunnel.dispose(); hostMap.delete(remote.port); } } @@ -120,31 +232,42 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe this._onDidChangeTunnels.fire(); } - $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined { + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { if (this._forwardPortProvider) { - const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); - if (providedPort !== undefined) { - return asPromise(() => providedPort).then(tunnel => { + try { + this.logService.trace('$forwardPort: Getting tunnel from provider.'); + const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions); + this.logService.trace('$forwardPort: Got tunnel promise from provider.'); + if (providedPort !== undefined) { + const tunnel = await providedPort; + this.logService.trace('$forwardPort: Successfully awaited tunnel from provider.'); if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) { this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map()); } const disposeListener = this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress))); this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, { tunnel, disposeListener }); - return Promise.resolve(TunnelDto.fromApiTunnel(tunnel)); - }); + return TunnelDto.fromApiTunnel(tunnel); + } else { + this.logService.trace('$forwardPort: Tunnel is undefined'); + } + } catch (e) { + this.logService.trace('$forwardPort: tunnel provider error'); } } return undefined; } + async $applyCandidateFilter(candidates: CandidatePort[]): Promise { + const filter = await Promise.all(candidates.map(candidate => this._showCandidatePort(candidate.host, candidate.port, candidate.detail))); + return candidates.filter((candidate, index) => filter[index]); + } - async findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> { - const ports: { host: string, port: number, detail: string }[] = []; + async findCandidatePorts(): Promise { let tcp: string = ''; let tcp6: string = ''; try { - tcp = fs.readFileSync('/proc/net/tcp', 'utf8'); - tcp6 = fs.readFileSync('/proc/net/tcp6', 'utf8'); + tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -154,105 +277,24 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe }); })); - const procChildren = fs.readdirSync('/proc'); - const processes: { pid: number, cwd: string, cmd: string }[] = []; + const procChildren = await pfs.readdir('/proc'); + const processes: { + pid: number, cwd: string, cmd: string + }[] = []; for (let childName of procChildren) { try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = fs.statSync(childUri.fsPath); + const childStat = await fs.promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = fs.readlinkSync(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { // } } - - const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6); - const sockets = this.getSockets(procSockets); - - const socketMap = sockets.reduce((m, socket) => { - m[socket.socket] = socket; - return m; - }, {} as Record); - const processMap = processes.reduce((m, process) => { - m[process.pid] = process; - return m; - }, {} as Record); - - connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { - const command = processMap[socketMap[socket].pid].cmd; - if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) { - ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); - } - }); - - return ports; - } - - private getSockets(stdout: string): { pid: number, socket: number }[] { - const lines = stdout.trim().split('\n'); - const mapped: { pid: number, socket: number }[] = []; - lines.forEach(line => { - const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; - if (match && match.length >= 3) { - mapped.push({ - pid: parseInt(match[1], 10), - socket: parseInt(match[2], 10) - }); - } - }); - return mapped; - } - - private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { - const table = ([] as Record[]).concat(...stdouts.map(this.loadConnectionTable)); - return [ - ...new Map( - table.filter(row => row.st === '0A') - .map(row => { - const address = row.local_address.split(':'); - return { - socket: parseInt(row.inode, 10), - ip: this.parseIpAddress(address[0]), - port: parseInt(address[1], 16) - }; - }).map(port => [port.ip + ':' + port.port, port]) - ).values() - ]; - } - - private parseIpAddress(hex: string): string { - let result = ''; - if (hex.length === 8) { - for (let i = hex.length - 2; i >= 0; i -= 2) { - result += parseInt(hex.substr(i, 2), 16); - if (i !== 0) { - result += '.'; - } - } - } else { - for (let i = hex.length - 4; i >= 0; i -= 4) { - result += parseInt(hex.substr(i, 4), 16).toString(16); - if (i !== 0) { - result += ':'; - } - } - } - return result; - } - - private loadConnectionTable(stdout: string): Record[] { - const lines = stdout.trim().split('\n'); - const names = lines.shift()!.trim().split(/\s+/) - .filter(name => name !== 'rx_queue' && name !== 'tm->when'); - const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { - obj[names[i] || i] = value; - return obj; - }, {} as Record)); - return table; + return findPorts(tcp, tcp6, procSockets, processes); } } diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 7f15f295d8..f8bd05bbc6 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -8,10 +8,38 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { timeout } from 'vs/base/common/async'; +namespace TrustedFunction { + + // workaround a chrome issue not allowing to create new functions + // see https://github.com/w3c/webappsec-trusted-types/wiki/Trusted-Types-for-function-constructor + const ttpTrustedFunction = self.trustedTypes?.createPolicy('TrustedFunctionWorkaround', { + createScript: (_, ...args: string[]) => { + args.forEach((arg) => { + if (!self.trustedTypes?.isScript(arg)) { + throw new Error('TrustedScripts only, please'); + } + }); + // NOTE: This is insecure without parsing the arguments and body, + // Malicious inputs can escape the function body and execute immediately! + const fnArgs = args.slice(0, -1).join(','); + const fnBody = args.pop()!.toString(); + const body = `(function anonymous(${fnArgs}) {\n${fnBody}\n})`; + return body; + } + }); + + export function create(...args: string[]): Function { + if (!ttpTrustedFunction) { + return new Function(...args); + } + return self.eval(ttpTrustedFunction.createScript('', ...args) as unknown as string); + } +} + class WorkerRequireInterceptor extends RequireInterceptor { _installInterceptor() { } @@ -35,6 +63,8 @@ class WorkerRequireInterceptor extends RequireInterceptor { export class ExtHostExtensionService extends AbstractExtHostExtensionService { readonly extensionRuntime = ExtensionRuntime.Webworker; + private static _ttpExtensionScripts = self.trustedTypes?.createPolicy('ExtensionScripts', { createScript: source => source }); + private _fakeModules?: WorkerRequireInterceptor; protected async _beforeAlmostReadyToRunExtensions(): Promise { @@ -42,6 +72,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); await this._fakeModules.install(); + performance.mark('code/extHost/didInitAPI'); + await this._waitForDebuggerAttachment(); } @@ -49,10 +81,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { return extensionDescription.browser; } - protected async _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected async _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { module = module.with({ path: ensureSuffix(module.path, '.js') }); + if (extensionId) { + performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`); + } const response = await fetch(module.toString(true)); + if (extensionId) { + performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`); + } if (response.status !== 200) { throw new Error(response.statusText); @@ -63,7 +101,25 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // Here we append #vscode-extension to serve as a marker, such that source maps // can be adjusted for the extra wrapping function. const sourceURL = `${module.toString(true)}#vscode-extension`; - const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`); + const fullSource = `${source}\n//# sourceURL=${sourceURL}`; + let initFn: Function; + try { + initFn = TrustedFunction.create( + ExtHostExtensionService._ttpExtensionScripts?.createScript('module') as unknown as string ?? 'module', + ExtHostExtensionService._ttpExtensionScripts?.createScript('exports') as unknown as string ?? 'exports', + ExtHostExtensionService._ttpExtensionScripts?.createScript('require') as unknown as string ?? 'require', + ExtHostExtensionService._ttpExtensionScripts?.createScript(fullSource) as unknown as string ?? fullSource + ); + } catch (err) { + if (extensionId) { + console.error(`Loading code for extension ${extensionId.value} failed: ${err.message}`); + } else { + console.error(`Loading code failed: ${err.message}`); + } + console.error(`${module.toString(true)}${typeof err.line === 'number' ? ` line ${err.line}` : ''}${typeof err.column === 'number' ? ` column ${err.column}` : ''}`); + console.error(err); + throw err; + } // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; @@ -78,9 +134,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { try { activationTimesBuilder.codeLoadingStart(); + if (extensionId) { + performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`); + } initFn(_module, _exports, _require); return (_module.exports !== _exports ? _module.exports : _exports); } finally { + if (extensionId) { + performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`); + } activationTimesBuilder.codeLoadingStop(); } } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 8d330e03e3..bc59a76cff 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -8,25 +8,21 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Action } from 'vs/base/common/actions'; import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; -import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; -import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { Codicon } from 'vs/base/common/codicons'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); @@ -125,54 +121,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 3 }); -// --- Toggle Editor Layout - -export class ToggleEditorLayoutAction extends Action { - - static readonly ID = 'workbench.action.toggleEditorGroupLayout'; - static readonly LABEL = nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService - ) { - super(id, label); - - this.class = Codicon.editorLayout.classNames; - this.updateEnablement(); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.add(this.editorGroupService.onDidAddGroup(() => this.updateEnablement())); - this.toDispose.add(this.editorGroupService.onDidRemoveGroup(() => this.updateEnablement())); - } - - private updateEnablement(): void { - this.enabled = this.editorGroupService.count > 1; - } - - async run(): Promise { - const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; - this.editorGroupService.setGroupOrientation(newOrientation); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorLayoutAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Toggle Vertical/Horizontal Editor Layout', CATEGORIES.View.value); - -MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { - group: 'z_flip', - command: { - id: ToggleEditorLayoutAction.ID, - title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") - }, - order: 1 -}); - // --- Toggle Sidebar Position export class ToggleSidebarPositionAction extends Action { @@ -203,7 +151,64 @@ export class ToggleSidebarPositionAction extends Action { } } -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarPositionAction), 'View: Toggle Side Bar Position', CATEGORIES.View.value); +registerAction2(class extends Action2 { + constructor() { + super({ + id: ToggleSidebarPositionAction.ID, + title: { value: nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"), original: 'Toggle Side Bar Position' }, + category: CATEGORIES.View, + f1: true + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IInstantiationService).createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL).run(); + } +}); +MenuRegistry.appendMenuItems([{ + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar right', "Move Side Bar Right") + }, + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar right', "Move Side Bar Right") + }, + when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar left', "Move Side Bar Left") + }, + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: ToggleSidebarPositionAction.ID, + title: nls.localize('move sidebar left', "Move Side Bar Left") + }, + when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 1 + } +}]); MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '3_workbench_layout_move', @@ -256,27 +261,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 5 }); -export class ToggleSidebarVisibilityAction extends Action { - - static readonly ID = 'workbench.action.toggleSidebarVisibility'; - static readonly LABEL = nls.localize('toggleSidebar', "Toggle Side Bar Visibility"); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, label); - } - - async run(): Promise { - const hideSidebar = this.layoutService.isVisible(Parts.SIDEBAR_PART); - this.layoutService.setSideBarHidden(hideSidebar); - } -} - -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarVisibilityAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', CATEGORIES.View.value); - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '2_appearance', title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"), @@ -284,10 +268,53 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { order: 1 }); +export const TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID = 'workbench.action.toggleSidebarVisibility'; +registerAction2(class extends Action2 { + constructor() { + super({ + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: { value: nls.localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' }, + category: CATEGORIES.View, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_B + } + }); + } + run(accessor: ServicesAccessor) { + const layoutService = accessor.get(IWorkbenchLayoutService); + layoutService.setSideBarHidden(layoutService.isVisible(Parts.SIDEBAR_PART)); + } +}); +MenuRegistry.appendMenuItems([{ + id: MenuId.ViewContainerTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + }, + when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 2 + } +}, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, + title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), + }, + when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), + order: 2 + } +}]); + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '2_workbench_layout', command: { - id: ToggleSidebarVisibilityAction.ID, + id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID, title: nls.localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), toggled: SideBarVisibleContext }, @@ -413,32 +440,16 @@ export class ToggleMenuBarAction extends Action { static readonly ID = 'workbench.action.toggleMenuBar'; static readonly LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar"); - private static readonly menuBarVisibilityKey = 'window.menuBarVisibility'; - constructor( id: string, label: string, - @IConfigurationService private readonly configurationService: IConfigurationService + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { super(id, label); } - run(): Promise { - let currentVisibilityValue = getMenuBarVisibility(this.configurationService); - if (typeof currentVisibilityValue !== 'string') { - currentVisibilityValue = 'default'; - } - - let newVisibilityValue: string; - if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') { - newVisibilityValue = 'toggle'; - } else if (currentVisibilityValue === 'compact') { - newVisibilityValue = 'hidden'; - } else { - newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default'; - } - - return this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); + async run(): Promise { + this.layoutService.toggleMenuBar(); } } @@ -478,37 +489,6 @@ export class ResetViewLocationsAction extends Action { registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetViewLocationsAction), 'View: Reset View Locations', CATEGORIES.View.value); -// --- Toggle View with Command -export abstract class ToggleViewAction extends Action { - - constructor( - id: string, - label: string, - private readonly viewId: string, - protected viewsService: IViewsService, - protected viewDescriptorService: IViewDescriptorService, - protected contextKeyService: IContextKeyService, - private layoutService: IWorkbenchLayoutService, - cssClass?: string - ) { - super(id, label, cssClass); - } - - async run(): Promise { - const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); - - if (focusedViewId === this.viewId) { - if (this.viewDescriptorService.getViewLocationById(this.viewId) === ViewContainerLocation.Sidebar) { - this.layoutService.setSideBarHidden(true); - } else { - this.layoutService.setPanelHidden(true); - } - } else { - this.viewsService.openView(this.viewId, true); - } - } -} - // --- Move View with Command export class MoveViewAction extends Action { static readonly ID = 'workbench.action.moveView'; @@ -698,7 +678,7 @@ export class MoveFocusedViewAction extends Action { .map(viewletId => { return { id: viewletId, - label: this.viewDescriptorService.getViewContainerById(viewletId)!.name + label: this.viewDescriptorService.getViewContainerById(viewletId)!.title }; })); @@ -719,7 +699,7 @@ export class MoveFocusedViewAction extends Action { .map(panel => { return { id: panel.id, - label: this.viewDescriptorService.getViewContainerById(panel.id)!.name + label: this.viewDescriptorService.getViewContainerById(panel.id)!.title }; })); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index ff914ee296..93218205ce 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -356,19 +356,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // List if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusPreviousPage(); - list.reveal(list.getFocus()[0]); + focused.focusPreviousPage(); } // Tree else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.focusPreviousPage(fakeKeyboardEvent); - list.reveal(list.getFocus()[0]); + focused.focusPreviousPage(fakeKeyboardEvent); } // Ensure DOM Focus @@ -386,19 +380,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // List if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusNextPage(); - list.reveal(list.getFocus()[0]); + focused.focusNextPage(); } // Tree else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.focusNextPage(fakeKeyboardEvent); - list.reveal(list.getFocus()[0]); + focused.focusNextPage(fakeKeyboardEvent); } // Ensure DOM Focus diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index 6eaab6e5d2..2b5a94f79f 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -17,7 +17,6 @@ import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/c import { Direction } from 'vs/base/browser/ui/grid/grid'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isAncestor } from 'vs/base/browser/dom'; abstract class BaseNavigationAction extends Action { @@ -215,7 +214,10 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part } function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorService: IEditorService, next: boolean): void { - const currentlyFocusedPart = isActiveElementInNotebookEditor(editorService) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : + // Need to ask if the active editor has focus since the layoutService is not aware of some custom editor focus behavior(notebooks) + // Also need to ask the layoutService for the case if no editor is opened + const editorFocused = editorService.activeEditorPane?.hasFocus() || layoutService.hasFocus(Parts.EDITOR_PART); + const currentlyFocusedPart = editorFocused ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined; let partToFocus = Parts.EDITOR_PART; if (currentlyFocusedPart) { @@ -225,17 +227,6 @@ function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorS layoutService.focusPart(partToFocus); } -function isActiveElementInNotebookEditor(editorService: IEditorService): boolean { - const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; - if (activeEditorPane?.isNotebookEditor) { - const control = editorService.activeEditorPane?.getControl() as { getDomNode(): HTMLElement; getOverflowContainerDomNode(): HTMLElement; }; - const activeElement = document.activeElement; - return isAncestor(activeElement, control.getDomNode()) || isAncestor(activeElement, control.getOverflowContainerDomNode()); - } - - return false; -} - export class FocusNextPart extends Action { static readonly ID = 'workbench.action.focusNextPart'; static readonly LABEL = nls.localize('focusNextPart', "Focus Next Part"); diff --git a/src/vs/workbench/browser/actions/textInputActions.ts b/src/vs/workbench/browser/actions/textInputActions.ts index afd734317b..7c9479c4e7 100644 --- a/src/vs/workbench/browser/actions/textInputActions.ts +++ b/src/vs/workbench/browser/actions/textInputActions.ts @@ -76,23 +76,26 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo // Context menu support in input/textarea this.layoutService.container.addEventListener('contextmenu', e => this.onContextMenu(e)); - } private onContextMenu(e: MouseEvent): void { - if (e.target instanceof HTMLElement) { - const target = e.target; - if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) { - EventHelper.stop(e, true); - - this.contextMenuService.showContextMenu({ - getAnchor: () => e, - getActions: () => this.textInputActions, - getActionsContext: () => target, - onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948 - }); - } + if (e.defaultPrevented) { + return; // make sure to not show these actions by accident if component indicated to prevent } + + const target = e.target; + if (!(target instanceof HTMLElement) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) { + return; // only for inputs or textareas + } + + EventHelper.stop(e, true); + + this.contextMenuService.showContextMenu({ + getAnchor: () => e, + getActions: () => this.textInputActions, + getActionsContext: () => target, + onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948 + }); } } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 718c3c734b..7b21b9eb20 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -33,7 +33,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { Codicon } from 'vs/base/common/codicons'; import { isHTMLElement } from 'vs/base/browser/dom'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const inRecentFilesPickerContextKey = 'inRecentFilesPicker'; @@ -49,12 +49,17 @@ abstract class BaseOpenRecentAction extends Action { tooltip: nls.localize('remove', "Remove from Recently Opened") }; - private readonly dirtyRecentlyOpened: IQuickInputButton = { + private readonly dirtyRecentlyOpenedFolder: IQuickInputButton = { iconClass: 'dirty-workspace ' + Codicon.closeDirty.classNames, - tooltip: nls.localize('dirtyRecentlyOpened', "Workspace With Dirty Files"), + tooltip: nls.localize('dirtyRecentlyOpenedFolder', "Folder With Unsaved Files"), alwaysVisible: true }; + private readonly dirtyRecentlyOpenedWorkspace: IQuickInputButton = { + ...this.dirtyRecentlyOpenedFolder, + tooltip: nls.localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"), + }; + constructor( id: string, label: string, @@ -77,7 +82,9 @@ abstract class BaseOpenRecentAction extends Action { const recentlyOpened = await this.workspacesService.getRecentlyOpened(); const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces(); - // Identify all folders and workspaces with dirty files + let hasWorkspaces = false; + + // Identify all folders and workspaces with unsaved files const dirtyFolders = new ResourceMap(); const dirtyWorkspaces = new ResourceMap(); for (const dirtyWorkspace of dirtyWorkspacesAndFolders) { @@ -85,6 +92,7 @@ abstract class BaseOpenRecentAction extends Action { dirtyFolders.set(dirtyWorkspace, true); } else { dirtyWorkspaces.set(dirtyWorkspace.configPath, dirtyWorkspace); + hasWorkspaces = true; } } @@ -96,6 +104,7 @@ abstract class BaseOpenRecentAction extends Action { recentFolders.set(recent.folderUri, true); } else { recentWorkspaces.set(recent.workspace.configPath, recent.workspace); + hasWorkspaces = true; } } @@ -124,7 +133,7 @@ abstract class BaseOpenRecentAction extends Action { let keyMods: IKeyMods | undefined; - const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") }; + const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: hasWorkspaces ? nls.localize('workspacesAndFolders', "folders & workspaces") : nls.localize('folders', "folders") }; const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") }; const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; @@ -143,13 +152,14 @@ abstract class BaseOpenRecentAction extends Action { context.removeItem(); } - // Dirty Workspace - else if (context.button === this.dirtyRecentlyOpened) { + // Dirty Folder/Workspace + else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) { + const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace; const result = await this.dialogService.confirm({ type: 'question', - title: nls.localize('dirtyWorkspace', "Workspace with Dirty Files"), - message: nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the dirty files?"), - detail: nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with dirty files cannot be removed until all dirty files have been saved or reverted.") + title: isDirtyWorkspace ? nls.localize('dirtyWorkspace', "Workspace with Unsaved Files") : nls.localize('dirtyFolder', "Folder with Unsaved Files"), + message: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : nls.localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"), + detail: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with unsaved files cannot be removed until all unsaved files have been saved or reverted.") : nls.localize('dirtyFolderConfirmDetail', "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.") }); if (result.confirmed) { @@ -170,6 +180,7 @@ abstract class BaseOpenRecentAction extends Action { let iconClasses: string[]; let fullLabel: string | undefined; let resource: URI | undefined; + let isWorkspace = false; // Folder if (isRecentFolder(recent)) { @@ -185,6 +196,7 @@ abstract class BaseOpenRecentAction extends Action { iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER); openable = { workspaceUri: resource }; fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true }); + isWorkspace = true; } // File @@ -200,9 +212,9 @@ abstract class BaseOpenRecentAction extends Action { return { iconClasses, label: name, - ariaLabel: isDirty ? nls.localize('recentDirtyAriaLabel', "{0}, dirty workspace", name) : name, + ariaLabel: isDirty ? isWorkspace ? nls.localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : nls.localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name, description: parentPath, - buttons: isDirty ? [this.dirtyRecentlyOpened] : [this.removeFromRecentlyOpened], + buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened], openable, resource }; @@ -405,7 +417,7 @@ CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', ac const configurationService = accessor.get(IConfigurationService); const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue; - return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never', ConfigurationTarget.USER); + return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never'); }); // --- Menu Registration diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index e7a3ed51c5..6dc00212ae 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -114,7 +114,7 @@ export class CloseWorkspaceAction extends Action { async run(): Promise { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - this.notificationService.info(nls.localize('noWorkspaceOpened', "There is currently no workspace opened in this instance to close.")); + this.notificationService.info(nls.localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close.")); return; } @@ -225,7 +225,7 @@ export class SaveWorkspaceAsAction extends Action { export class DuplicateWorkspaceInNewWindowAction extends Action { static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow'; - static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window"); + static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window"); constructor( id: string, @@ -259,7 +259,7 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(AddRootFolderAction), registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalRemoveRootFolderAction), 'Workspaces: Remove Folder from Workspace...', workspacesCategory); registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseWorkspaceAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, EmptyWorkspaceSupportContext); registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveWorkspaceAsAction), 'Workspaces: Save Workspace As...', workspacesCategory, EmptyWorkspaceSupportContext); -registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate Workspace in New Window', workspacesCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate As Workspace in New Window', workspacesCategory); // --- Menu Registration diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index f47d290756..0970225815 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -131,7 +131,7 @@ interface IOpenFolderAPICommandOptions { CommandsRegistry.registerCommand({ id: 'vscode.openFolder', - handler: (accessor: ServicesAccessor, uri?: URI, arg: boolean | IOpenFolderAPICommandOptions = {}) => { + handler: (accessor: ServicesAccessor, uri?: URI, arg?: boolean | IOpenFolderAPICommandOptions) => { const commandService = accessor.get(ICommandService); // Be compatible to previous args by converting to options @@ -141,15 +141,15 @@ CommandsRegistry.registerCommand({ // Without URI, ask to pick a folder or workpsace to open if (!uri) { - return commandService.executeCommand('_files.pickFolderAndOpen', { forceNewWindow: arg.forceNewWindow }); + return commandService.executeCommand('_files.pickFolderAndOpen', { forceNewWindow: arg?.forceNewWindow }); } uri = URI.revive(uri); const options: IOpenWindowOptions = { - forceNewWindow: arg.forceNewWindow, - forceReuseWindow: arg.forceReuseWindow, - noRecentEntry: arg.noRecentEntry + forceNewWindow: arg?.forceNewWindow, + forceReuseWindow: arg?.forceReuseWindow, + noRecentEntry: arg?.noRecentEntry }; const uriToOpen: IWindowOpenable = (hasWorkspaceFileExtension(uri) || uri.scheme === Schemas.untitled) ? { workspaceUri: uri } : { folderUri: uri }; diff --git a/src/vs/workbench/browser/parts/editor/editorWidgets.ts b/src/vs/workbench/browser/codeeditor.ts similarity index 60% rename from src/vs/workbench/browser/parts/editor/editorWidgets.ts rename to src/vs/workbench/browser/codeeditor.ts index 8f433e9c45..3365c201ef 100644 --- a/src/vs/workbench/browser/parts/editor/editorWidgets.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Widget } from 'vs/base/browser/ui/widget'; -import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { Emitter } from 'vs/base/common/event'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -15,11 +15,121 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IRange } from 'vs/editor/common/core/range'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; + +export interface IRangeHighlightDecoration { + resource: URI; + range: IRange; + isWholeLine?: boolean; +} + +export class RangeHighlightDecorations extends Disposable { + + private readonly _onHighlightRemoved = this._register(new Emitter()); + readonly onHighlightRemoved = this._onHighlightRemoved.event; + + private rangeHighlightDecorationId: string | null = null; + private editor: ICodeEditor | null = null; + private readonly editorDisposables = this._register(new DisposableStore()); + + constructor(@IEditorService private readonly editorService: IEditorService) { + super(); + } + + removeHighlightRange() { + if (this.editor?.getModel() && this.rangeHighlightDecorationId) { + this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); + this._onHighlightRemoved.fire(); + } + + this.rangeHighlightDecorationId = null; + } + + highlightRange(range: IRangeHighlightDecoration, editor?: any) { + editor = editor ?? this.getEditor(range); + if (isCodeEditor(editor)) { + this.doHighlightRange(editor, range); + } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { + this.doHighlightRange(editor.activeCodeEditor, range); + } + } + + private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) { + this.removeHighlightRange(); + + editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { + this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine)); + }); + + this.setEditor(editor); + } + + private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined { + const activeEditor = this.editorService.activeEditor; + const resource = activeEditor?.resource; + if (resource && isEqual(resource, resourceRange.resource)) { + return this.editorService.activeTextEditorControl as ICodeEditor; + } + + return undefined; + } + + private setEditor(editor: ICodeEditor) { + if (this.editor !== editor) { + this.editorDisposables.clear(); + this.editor = editor; + this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { + if ( + e.reason === CursorChangeReason.NotSet + || e.reason === CursorChangeReason.Explicit + || e.reason === CursorChangeReason.Undo + || e.reason === CursorChangeReason.Redo + ) { + this.removeHighlightRange(); + } + })); + this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); })); + this.editorDisposables.add(this.editor.onDidDispose(() => { + this.removeHighlightRange(); + this.editor = null; + })); + } + } + + private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'rangeHighlight', + isWholeLine: true + }); + + private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'rangeHighlight' + }); + + private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions { + return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT); + } + + dispose() { + super.dispose(); + + if (this.editor?.getModel()) { + this.removeHighlightRange(); + this.editor = null; + } + } +} export class FloatingClickWidget extends Widget implements IOverlayWidget { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index c848bc0b2b..822535fbd1 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -56,15 +56,26 @@ export abstract class Composite extends Component implements IComposite { return this._onDidBlur.event; } + private _hasFocus = false; + hasFocus(): boolean { + return this._hasFocus; + } + private registerFocusTrackEvents(): { onDidFocus: Emitter, onDidBlur: Emitter } { const container = assertIsDefined(this.getContainer()); const focusTracker = this._register(trackFocus(container)); const onDidFocus = this._onDidFocus = this._register(new Emitter()); - this._register(focusTracker.onDidFocus(() => onDidFocus.fire())); + this._register(focusTracker.onDidFocus(() => { + this._hasFocus = true; + onDidFocus.fire(); + })); const onDidBlur = this._onDidBlur = this._register(new Emitter()); - this._register(focusTracker.onDidBlur(() => onDidBlur.fire())); + this._register(focusTracker.onDidBlur(() => { + this._hasFocus = false; + onDidBlur.fire(); + })); return { onDidFocus, onDidBlur }; } @@ -234,7 +245,6 @@ export abstract class CompositeDescriptor { readonly cssClass?: string, readonly order?: number, readonly requestedIndex?: number, - readonly keybindingId?: string, ) { } instantiate(instantiationService: IInstantiationService): T { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index f0714bfedd..b2417ddaa7 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { PanelPositionContext } from 'vs/workbench/common/panel'; +import { PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; @@ -64,6 +64,8 @@ export class WorkbenchContextKeysHandler extends Disposable { private sideBarVisibleContext: IContextKey; private editorAreaVisibleContext: IContextKey; private panelPositionContext: IContextKey; + private panelVisibleContext: IContextKey; + private panelMaximizedContext: IContextKey; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -146,9 +148,13 @@ export class WorkbenchContextKeysHandler extends Disposable { // Sidebar this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService); - // Panel Position + // Panel this.panelPositionContext = PanelPositionContext.bindTo(this.contextKeyService); this.panelPositionContext.set(positionToString(this.layoutService.getPanelPosition())); + this.panelVisibleContext = PanelVisibleContext.bindTo(this.contextKeyService); + this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); + this.panelMaximizedContext = PanelMaximizedContext.bindTo(this.contextKeyService); + this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); this.registerListeners(); } @@ -182,7 +188,11 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.viewletService.onDidViewletClose(() => this.updateSideBarContextKeys())); this._register(this.viewletService.onDidViewletOpen(() => this.updateSideBarContextKeys())); - this._register(this.layoutService.onPartVisibilityChange(() => this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)))); + this._register(this.layoutService.onPartVisibilityChange(() => { + this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)); + this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART)); + this.panelMaximizedContext.set(this.layoutService.isPanelMaximized()); + })); this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty))); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 4ec2a9fa1a..1ad8f406d1 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -582,7 +582,7 @@ export class CompositeDragAndDropObserver extends Disposable { const id = e.dragAndDropData.getData().id; const type = e.dragAndDropData.getData().type; const data = this.readDragData(type); - if (data && data.getData().id === id) { + if (data?.getData().id === id) { this.transferData.clearData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); } })); @@ -737,7 +737,7 @@ export class CompositeDragAndDropObserver extends Disposable { if (callbacks.onDragEnd) { this._onDragEnd.event(e => { callbacks.onDragEnd!(e); - }); + }, this, disposableStore); } return this._register(disposableStore); } diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index f81d705287..6f64aee2d0 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -12,7 +12,6 @@ import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { - getId(): string; getName(): string; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index bb4a7e5dfc..fbe203beab 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -22,6 +22,7 @@ import { getIconClasses, detectModeId } from 'vs/editor/common/services/getIconC import { Disposable, dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; export interface IResourceLabelProps { resource?: URI | { primary?: URI, secondary?: URI }; @@ -100,6 +101,10 @@ export const DEFAULT_LABELS_CONTAINER: IResourceLabelsContainer = { }; export class ResourceLabels extends Disposable { + + private _onDidChangeDecorations = this._register(new Emitter()); + readonly onDidChangeDecorations = this._onDidChangeDecorations.event; + private widgets: ResourceLabelWidget[] = []; private labels: IResourceLabel[] = []; @@ -148,7 +153,18 @@ export class ResourceLabels extends Disposable { })); // notify when file decoration changes - this._register(this.decorationsService.onDidChangeDecorations(e => this.widgets.forEach(widget => widget.notifyFileDecorationsChanges(e)))); + this._register(this.decorationsService.onDidChangeDecorations(e => { + let notifyDidChangeDecorations = false; + this.widgets.forEach(widget => { + if (widget.notifyFileDecorationsChanges(e)) { + notifyDidChangeDecorations = true; + } + }); + + if (notifyDidChangeDecorations) { + this._onDidChangeDecorations.fire(); + } + })); // notify when theme changes this._register(this.themeService.onDidColorThemeChange(() => this.widgets.forEach(widget => widget.notifyThemeChange()))); @@ -311,19 +327,21 @@ class ResourceLabelWidget extends IconLabel { } } - notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): void { + notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): boolean { if (!this.options) { - return; + return false; } const resource = toResource(this.label); if (!resource) { - return; + return false; } if (this.options.fileDecorations && e.affectsResource(resource)) { - this.render(false); + return this.render(false); } + + return false; } notifyExtensionsRegistered(): void { @@ -351,10 +369,10 @@ class ResourceLabelWidget extends IconLabel { } setFile(resource: URI, options?: IFileLabelOptions): void { - const hideLabel = options && options.hideLabel; + const hideLabel = options?.hideLabel; let name: string | undefined; if (!hideLabel) { - if (options && options.fileKind === FileKind.ROOT_FOLDER) { + if (options?.fileKind === FileKind.ROOT_FOLDER) { const workspaceFolder = this.contextService.getWorkspaceFolder(resource); if (workspaceFolder) { name = workspaceFolder.name; @@ -362,7 +380,7 @@ class ResourceLabelWidget extends IconLabel { } if (!name) { - name = basenameOrAuthority(resource); + name = normalizeDriveLetter(basenameOrAuthority(resource)); } } @@ -465,7 +483,7 @@ class ResourceLabelWidget extends IconLabel { this.setLabel(''); } - private render(clearIconCache: boolean): void { + private render(clearIconCache: boolean): boolean { if (this.isHidden) { if (!this.needsRedraw) { this.needsRedraw = clearIconCache ? Redraw.Full : Redraw.Basic; @@ -475,7 +493,7 @@ class ResourceLabelWidget extends IconLabel { this.needsRedraw = Redraw.Full; } - return; + return false; } if (this.label) { @@ -492,7 +510,7 @@ class ResourceLabelWidget extends IconLabel { } if (!this.label) { - return; + return false; } this.renderDisposables.clear(); @@ -528,11 +546,11 @@ class ResourceLabelWidget extends IconLabel { iconLabelOptions.extraClasses = this.computedIconClasses.slice(0); } - if (this.options && this.options.extraClasses) { + if (this.options?.extraClasses) { iconLabelOptions.extraClasses.push(...this.options.extraClasses); } - if (this.options && this.options.fileDecorations && resource) { + if (this.options?.fileDecorations && resource) { const deco = this.decorationsService.getDecoration( resource, this.options.fileKind !== FileKind.FILE @@ -558,6 +576,8 @@ class ResourceLabelWidget extends IconLabel { this.setLabel(label || '', this.label.description, iconLabelOptions); this._onDidRender.fire(); + + return true; } dispose(): void { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 44febd1d04..9fca918e7e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -47,6 +47,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { mark } from 'vs/base/common/performance'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { Promises } from 'vs/base/common/async'; export enum Settings { ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', @@ -78,7 +79,9 @@ enum Storage { GRID_LAYOUT = 'workbench.grid.layout', GRID_WIDTH = 'workbench.grid.width', - GRID_HEIGHT = 'workbench.grid.height' + GRID_HEIGHT = 'workbench.grid.height', + + MENU_VISIBILITY = 'window.menuBarVisibility' } enum Classes { @@ -318,12 +321,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (visible !== this.state.menuBar.toggled) { this.state.menuBar.toggled = visible; - if (this.state.fullscreen && (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'default')) { + if (this.state.fullscreen && (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'classic')) { // Propagate to grid this.workbenchGrid.setViewVisible(this.titleBarPartView, this.isVisible(Parts.TITLEBAR_PART)); - - this.layout(); } + + // Move layout call to any time the menubar + // is toggled to update consumers of offset + // see issue #115267 + this.layout(); } } @@ -622,7 +628,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this._openedDefaultEditors; } - private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[] } | undefined { + private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[]; } | undefined { const defaultLayout = this.environmentService.options?.defaultLayout; if (defaultLayout?.editors?.length && this.storageService.isNew(StorageScope.WORKSPACE)) { this._openedDefaultEditors = true; @@ -652,7 +658,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore editors restorePromises.push((async () => { - mark('willRestoreEditors'); + mark('code/willRestoreEditors'); // first ensure the editor part is restored await this.editorGroupService.whenRestored; @@ -669,17 +675,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi await this.editorService.openEditors(editors); } - mark('didRestoreEditors'); + mark('code/didRestoreEditors'); })()); // Restore default views const restoreDefaultViewsPromise = (async () => { if (this.state.views.defaults?.length) { - mark('willOpenDefaultViews'); + mark('code/willOpenDefaultViews'); - let locationsRestored: { id: string; order: number }[] = []; + let locationsRestored: { id: string; order: number; }[] = []; - const tryOpenView = (view: { id: string; order: number }): boolean => { + const tryOpenView = (view: { id: string; order: number; }): boolean => { const location = this.viewDescriptorService.getViewLocationById(view.id); if (location !== null) { const container = this.viewDescriptorService.getViewContainerByViewId(view.id); @@ -733,7 +739,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.panel.panelToRestore = locationsRestored[ViewContainerLocation.Panel].id; } - mark('didOpenDefaultViews'); + mark('code/didOpenDefaultViews'); } })(); restorePromises.push(restoreDefaultViewsPromise); @@ -748,14 +754,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; } - mark('willRestoreViewlet'); + mark('code/willRestoreViewlet'); const viewlet = await this.viewletService.openViewlet(this.state.sideBar.viewletToRestore); if (!viewlet) { await this.viewletService.openViewlet(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); // fallback to default viewlet as needed } - mark('didRestoreViewlet'); + mark('code/didRestoreViewlet'); })()); // Restore Panel @@ -768,14 +774,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; } - mark('willRestorePanel'); + mark('code/willRestorePanel'); const panel = await this.panelService.openPanel(this.state.panel.panelToRestore!); if (!panel) { await this.panelService.openPanel(Registry.as(PanelExtensions.Panels).getDefaultPanelId()); // fallback to default panel as needed } - mark('didRestorePanel'); + mark('code/didRestorePanel'); })()); // Restore Zen Mode @@ -789,7 +795,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Await restore to be done - await Promise.all(restorePromises); + await Promises.settled(restorePromises); } private updatePanelPosition() { @@ -886,7 +892,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return false; } else if (this.state.menuBar.visibility === 'visible') { return true; - } else if (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'default') { + } else if (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'classic') { return this.state.menuBar.toggled; } @@ -1128,7 +1134,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi [Parts.STATUSBAR_PART]: this.statusBarPartView }; - const fromJSON = ({ type }: { type: Parts }) => viewMap[type]; + const fromJSON = ({ type }: { type: Parts; }) => viewMap[type]; const workbenchGrid = SerializableGrid.deserialize( this.createGridDescriptor(), { fromJSON }, @@ -1410,7 +1416,29 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // If panel part becomes visible, show last active panel or default panel else if (!hidden && !this.panelService.getActivePanel()) { - const panelToOpen = this.panelService.getLastActivePanelId(); + let panelToOpen: string | undefined = this.panelService.getLastActivePanelId(); + const hasViews = (id: string): boolean => { + const viewContainer = this.viewDescriptorService.getViewContainerById(id); + if (!viewContainer) { + return false; + } + + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + if (!viewContainerModel) { + return false; + } + + return viewContainerModel.activeViewDescriptors.length >= 1; + }; + + // verify that the panel we try to open has views before we default to it + // otherwise fall back to any view that has views still refs #111463 + if (!panelToOpen || !hasViews(panelToOpen)) { + panelToOpen = this.viewDescriptorService + .getViewContainersByLocation(ViewContainerLocation.Panel) + .find(viewContainer => hasViews(viewContainer.id))?.id; + } + if (panelToOpen) { const focus = !skipLayout; this.panelService.openPanel(panelToOpen, focus); @@ -1529,6 +1557,24 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this.state.menuBar.visibility; } + toggleMenuBar(): void { + let currentVisibilityValue = getMenuBarVisibility(this.configurationService); + if (typeof currentVisibilityValue !== 'string') { + currentVisibilityValue = 'classic'; + } + + let newVisibilityValue: string; + if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'classic') { + newVisibilityValue = 'toggle'; + } else if (currentVisibilityValue === 'compact') { + newVisibilityValue = 'hidden'; + } else { + newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'classic'; + } + + this.configurationService.updateValue(Storage.MENU_VISIBILITY, newVisibilityValue); + } + getPanelPosition(): Position { return this.state.panel.position; } diff --git a/src/vs/workbench/browser/menuActions.ts b/src/vs/workbench/browser/menuActions.ts new file mode 100644 index 0000000000..fd877df272 --- /dev/null +++ b/src/vs/workbench/browser/menuActions.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MenuId, IMenuService, IMenu, SubmenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +class MenuActions extends Disposable { + + private readonly menu: IMenu; + + private _primaryActions: IAction[] = []; + get primaryActions() { return this._primaryActions; } + + private _secondaryActions: IAction[] = []; + get secondaryActions() { return this._secondaryActions; } + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private disposables = this._register(new DisposableStore()); + + constructor( + menuId: MenuId, + private readonly options: IMenuActionOptions | undefined, + private readonly menuService: IMenuService, + private readonly contextKeyService: IContextKeyService + ) { + super(); + this.menu = this._register(menuService.createMenu(menuId, contextKeyService)); + this._register(this.menu.onDidChange(() => this.updateActions())); + this.updateActions(); + } + + private updateActions(): void { + this.disposables.clear(); + this._primaryActions = []; + this._secondaryActions = []; + this.disposables.add(createAndFillInActionBarActions(this.menu, this.options, { primary: this._primaryActions, secondary: this._secondaryActions })); + this.disposables.add(this.updateSubmenus([...this._primaryActions, ...this._secondaryActions], {})); + this._onDidChange.fire(); + } + + private updateSubmenus(actions: readonly IAction[], submenus: { [id: number]: IMenu }): IDisposable { + const disposables = new DisposableStore(); + for (const action of actions) { + if (action instanceof SubmenuItemAction && !submenus[action.item.submenu.id]) { + const menu = submenus[action.item.submenu.id] = disposables.add(this.menuService.createMenu(action.item.submenu, this.contextKeyService)); + disposables.add(menu.onDidChange(() => this.updateActions())); + disposables.add(this.updateSubmenus(action.actions, submenus)); + } + } + return disposables; + } +} + +export class CompositeMenuActions extends Disposable { + + private readonly menuActions: MenuActions; + private readonly contextMenuActionsDisposable = this._register(new MutableDisposable()); + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + menuId: MenuId, + private readonly contextMenuId: MenuId | undefined, + private readonly options: IMenuActionOptions | undefined, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + ) { + super(); + this.menuActions = this._register(new MenuActions(menuId, this.options, menuService, contextKeyService)); + this._register(this.menuActions.onDidChange(() => this._onDidChange.fire())); + } + + getPrimaryActions(): IAction[] { + return this.menuActions.primaryActions; + } + + getSecondaryActions(): IAction[] { + return this.menuActions.secondaryActions; + } + + getContextMenuActions(): IAction[] { + const actions: IAction[] = []; + if (this.contextMenuId) { + const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService); + this.contextMenuActionsDisposable.value = createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions }); + menu.dispose(); + } + return actions; + } +} diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index f852dce8a5..af0f5a2658 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -16,13 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ViewPaneContainer } from './parts/views/viewPaneContainer'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; -import { ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { MenuId } from 'vs/platform/actions/common/actions'; export class PaneComposite extends Composite implements IPaneComposite { - private menuActions: ViewContainerMenuActions; - constructor( id: string, protected readonly viewPaneContainer: ViewPaneContainer, @@ -35,8 +31,6 @@ export class PaneComposite extends Composite implements IPaneComposite { @IWorkspaceContextService protected contextService: IWorkspaceContextService ) { super(id, telemetryService, themeService, storageService); - - this.menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.getId(), MenuId.ViewContainerTitleContext)); this._register(this.viewPaneContainer.onTitleAreaUpdate(() => this.updateTitleArea())); } @@ -71,22 +65,36 @@ export class PaneComposite extends Composite implements IPaneComposite { getContextMenuActions(): ReadonlyArray { const result = []; - result.push(...this.menuActions.getContextMenuActions()); + result.push(...this.viewPaneContainer.getContextMenuActions2()); - if (result.length) { + const otherActions = this.viewPaneContainer.getContextMenuActions(); + + if (otherActions.length) { result.push(new Separator()); + result.push(...otherActions); } - result.push(...this.viewPaneContainer.getContextMenuActions()); return result; } getActions(): ReadonlyArray { - return this.viewPaneContainer.getActions(); + const result = []; + result.push(...this.viewPaneContainer.getActions2()); + result.push(...this.viewPaneContainer.getActions()); + return result; } getSecondaryActions(): ReadonlyArray { - return this.viewPaneContainer.getSecondaryActions(); + const menuActions = this.viewPaneContainer.getSecondaryActions2(); + const viewPaneContainerActions = this.viewPaneContainer.getSecondaryActions(); + if (menuActions.length && viewPaneContainerActions.length) { + return [ + ...menuActions, + new Separator(), + ...viewPaneContainerActions + ]; + } + return menuActions.length ? menuActions : viewPaneContainerActions; } getActionViewItem(action: IAction): IActionViewItem | undefined { diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index cd49d5827e..4dd54f4ffb 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -6,23 +6,74 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IPanel } from 'vs/workbench/common/panel'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; -import { IConstructorSignature0, BrandedService } from 'vs/platform/instantiation/common/instantiation'; +import { IConstructorSignature0, BrandedService, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { assertIsDefined } from 'vs/base/common/types'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -export abstract class Panel extends PaneComposite implements IPanel { } +export abstract class Panel extends PaneComposite implements IPanel { + + private readonly panelActions: CompositeMenuActions; + + constructor(id: string, + viewPaneContainer: ViewPaneContainer, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + ) { + super(id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this.panelActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, MenuId.PanelTitle, undefined, undefined)); + this._register(this.panelActions.onDidChange(() => this.updateTitleArea())); + } + + getActions(): ReadonlyArray { + return [...super.getActions(), ...this.panelActions.getPrimaryActions()]; + } + + getSecondaryActions(): ReadonlyArray { + return this.mergeSecondaryActions(super.getSecondaryActions(), this.panelActions.getSecondaryActions()); + } + + getContextMenuActions(): ReadonlyArray { + return this.mergeSecondaryActions(super.getContextMenuActions(), this.panelActions.getContextMenuActions()); + } + + private mergeSecondaryActions(actions: ReadonlyArray, panelActions: IAction[]): ReadonlyArray { + if (panelActions.length && actions.length) { + return [ + ...actions, + new Separator(), + ...panelActions, + ]; + } + return panelActions.length ? panelActions : actions; + } +} /** * A panel descriptor is a leightweight descriptor of a panel in the workbench. */ export class PanelDescriptor extends CompositeDescriptor { - static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string): PanelDescriptor { - return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, requestedIndex, _commandId); + static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number): PanelDescriptor { + return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, requestedIndex); } - private constructor(ctor: IConstructorSignature0, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string) { - super(ctor, id, name, cssClass, order, requestedIndex, _commandId); + private constructor(ctor: IConstructorSignature0, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number) { + super(ctor, id, name, cssClass, order, requestedIndex); } } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 0222784e2e..582f9ebf1f 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -159,10 +159,10 @@ class PartLayout { // Title Size: Width (Fill), Height (Variable) let titleSize: Dimension; - if (this.options && this.options.hasTitle) { + if (this.options.hasTitle) { titleSize = new Dimension(width, Math.min(height, PartLayout.TITLE_HEIGHT)); } else { - titleSize = new Dimension(0, 0); + titleSize = Dimension.None; } let contentWidth = width; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index de798dc0b8..37a1d81559 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/activityaction'; -import * as nls from 'vs/nls'; -import * as DOM from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { EventType, addDisposableListener, EventHelper } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Action, IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenuService, MenuId, IMenu, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActivityAction, ActivityActionViewItem, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IActivity } from 'vs/workbench/common/activity'; @@ -29,12 +29,12 @@ import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { AuthenticationSession } from 'vs/editor/common/modes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export class ViewContainerActivityAction extends ActivityAction { @@ -97,10 +97,10 @@ export class ViewContainerActivityAction extends ActivityAction { private logAction(action: string) { type ActivityBarActionClassification = { - viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + action: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; }; - this.telemetryService.publicLog2<{ viewletId: String, action: String }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action }); + this.telemetryService.publicLog2<{ viewletId: String, action: String; }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action }); } } @@ -109,6 +109,7 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { constructor( private readonly menuId: MenuId, action: ActivityAction, + private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService protected readonly menuService: IMenuService, @@ -126,30 +127,35 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { // Context menus are triggered on mouse down so that an item can be picked // and executed with releasing the mouse over it - this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { - DOM.EventHelper.stop(e, true); + this._register(addDisposableListener(this.container, EventType.MOUSE_DOWN, (e: MouseEvent) => { + EventHelper.stop(e, true); this.showContextMenu(e); })); - this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + this._register(addDisposableListener(this.container, EventType.KEY_UP, (e: KeyboardEvent) => { let event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - DOM.EventHelper.stop(e, true); + EventHelper.stop(e, true); this.showContextMenu(); } })); - this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { - DOM.EventHelper.stop(e, true); + this._register(addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { + EventHelper.stop(e, true); this.showContextMenu(); })); } - protected async showContextMenu(e?: MouseEvent): Promise { + private async showContextMenu(e?: MouseEvent): Promise { const disposables = new DisposableStore(); - const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); - const actions = await this.resolveActions(menu, disposables); + let actions: IAction[]; + if (e?.button !== 2) { + const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); + actions = await this.resolveMainMenuActions(menu, disposables); + } else { + actions = await this.resolveContextMenuActions(disposables); + } const isUsingCustomMenu = isWeb || (getTitleBarStyle(this.configurationService) !== 'native' && !isMacintosh); // see #40262 const position = this.configurationService.getValue('workbench.sideBar.location'); @@ -163,13 +169,17 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { }); } - protected async resolveActions(menu: IMenu, disposables: DisposableStore): Promise { + protected async resolveMainMenuActions(menu: IMenu, disposables: DisposableStore): Promise { const actions: IAction[] = []; disposables.add(createAndFillInActionBarActions(menu, undefined, { primary: [], secondary: actions })); return actions; } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + return this.contextMenuActionsProvider(); + } } export class HomeActivityActionViewItem extends MenuActivityActionViewItem { @@ -179,6 +189,7 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem { constructor( private readonly goHomeHref: string, action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService menuService: IMenuService, @@ -188,27 +199,32 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IStorageService private readonly storageService: IStorageService ) { - super(MenuId.MenubarHomeMenu, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.MenubarHomeMenu, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } - protected async resolveActions(homeMenu: IMenu, disposables: DisposableStore): Promise { + protected async resolveMainMenuActions(homeMenu: IMenu, disposables: DisposableStore): Promise { const actions = []; // Go Home - actions.push(disposables.add(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, async () => window.location.href = this.goHomeHref))); - actions.push(disposables.add(new Separator())); + actions.push(toAction({ id: 'goHome', label: localize('goHome', "Go Home"), run: () => window.location.href = this.goHomeHref })); // Contributed - const contributedActions = await super.resolveActions(homeMenu, disposables); - actions.push(...contributedActions); - - // Hide - if (contributedActions.length > 0) { + const contributedActions = await super.resolveMainMenuActions(homeMenu, disposables); + if (contributedActions.length) { actions.push(disposables.add(new Separator())); + actions.push(...contributedActions); } - actions.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => { - this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER); - }))); + + return actions; + } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideHomeButton', label: localize('hideHomeButton', "Hide Home Button"), run: () => this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER) }), + new Separator() + ]); return actions; } @@ -220,6 +236,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { constructor( action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @@ -227,15 +244,15 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { @IContextKeyService contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IStorageService private readonly storageService: IStorageService, @IProductService private readonly productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IStorageService private readonly storageService: IStorageService ) { - super(MenuId.AccountsContext, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.AccountsContext, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } - protected async resolveActions(accountsMenu: IMenu, disposables: DisposableStore): Promise { - await super.resolveActions(accountsMenu, disposables); + protected async resolveMainMenuActions(accountsMenu: IMenu, disposables: DisposableStore): Promise { + await super.resolveMainMenuActions(accountsMenu, disposables); const otherCommands = accountsMenu.getActions(); const providers = this.authenticationService.getProviderIds(); @@ -243,7 +260,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { try { const sessions = await this.authenticationService.getSessions(providerId); - const groupedSessions: { [label: string]: AuthenticationSession[] } = {}; + const groupedSessions: { [label: string]: AuthenticationSession[]; } = {}; sessions.forEach(session => { if (groupedSessions[session.account.label]) { groupedSessions[session.account.label].push(session); @@ -266,11 +283,11 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { if (sessionInfo.sessions) { Object.keys(sessionInfo.sessions).forEach(accountName => { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => { + const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => { return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); })); - const signOutAction = disposables.add(new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, () => { + const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), '', true, () => { return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName); })); @@ -285,7 +302,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { menus.push(providerSubMenu); }); } else { - const providerUnavailableAction = disposables.add(new Action('providerUnavailable', nls.localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName))); + const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName))); menus.push(providerUnavailableAction); } }); @@ -302,22 +319,26 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { } }); - if (menus.length) { - menus.push(disposables.add(new Separator())); - } - - menus.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => { - this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - }))); - return menus; } + + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER) }), + new Separator() + ]); + + return actions; + } } export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { constructor( action: ActivityAction, + contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, @IThemeService themeService: IThemeService, @IMenuService menuService: IMenuService, @@ -326,7 +347,7 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(MenuId.GlobalActivity, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); + super(MenuId.GlobalActivity, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService); } } @@ -379,7 +400,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.previousSideBarView', - title: { value: nls.localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, + title: { value: localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, category: CATEGORIES.View, f1: true }, -1); @@ -392,7 +413,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.nextSideBarView', - title: { value: nls.localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, + title: { value: localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, category: CATEGORIES.View, f1: true }, 1); @@ -400,7 +421,7 @@ registerAction2( } ); -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const activityBarBackgroundColor = theme.getColor(ACTIVITY_BAR_BACKGROUND); if (activityBarBackgroundColor) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index d39ecc335d..8e95a7cc8b 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/activitybarpart'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; @@ -13,7 +13,7 @@ import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; -import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ToggleActivityBarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -23,35 +23,34 @@ import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions'; -import { IViewDescriptorService, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; -import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { assertIsDefined } from 'vs/base/common/types'; +import { IViewDescriptorService, ViewContainer, IViewContainerModel, ViewContainerLocation, IViewsService, getEnabledViewContainerContextKey } from 'vs/workbench/common/views'; +import { IContextKeyService, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { assertIsDefined, isString } from 'vs/base/common/types'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { isWeb } from 'vs/base/common/platform'; +import { isNative, isWeb } from 'vs/base/common/platform'; import { Before2D } from 'vs/workbench/browser/dnd'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; -import { Action, Separator } from 'vs/base/common/actions'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -import { AccountsActionViewItem } from 'sql/workbench/browser/parts/activitybar/activitybarActions'; // {{SQL CARBON EDIT}} - use the ADS account management action +import { StringSHA1 } from 'vs/base/common/hash'; interface IPlaceholderViewContainer { readonly id: string; readonly name?: string; readonly iconUrl?: UriComponents; readonly themeIcon?: ThemeIcon; - readonly views?: { when?: string }[]; + readonly isBuiltin?: boolean; + readonly views?: { when?: string; }[]; } interface IPinnedViewContainer { @@ -68,11 +67,11 @@ interface ICachedViewContainer { readonly pinned: boolean; readonly order?: number; visible: boolean; - views?: { when?: string }[]; + isBuiltin?: boolean; + views?: { when?: string; }[]; } -export const settingsViewBarIcon = registerIcon('settings-view-bar-icon', Codicon.settingsGear, nls.localize('settingsViewBarIcon', 'Settings icon in the view bar.')); // {{SQL CARBON EDIT}} exporting to use it in getting started tour -const accountsViewBarIcon = registerIcon('accounts-view-bar-icon', Codicon.account, nls.localize('accountsViewBarIcon', 'Accounts icon in the view bar.')); +export const settingsViewBarIcon = registerIcon('settings-view-bar-icon', Codicon.settingsGear, localize('settingsViewBarIcon', "Settings icon in the view bar.")); // {{SQL CARBON EDIT}} exporting to use it in getting started tour export class ActivitybarPart extends Part implements IActivityBarService { @@ -83,6 +82,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { private static readonly ACTION_HEIGHT = 48; private static readonly ACCOUNTS_ACTION_INDEX = 0; + private static readonly GEAR_ICON = settingsViewBarIcon; // registerIcon('settings-view-bar-icon', Codicon.settingsGear, localize('settingsViewBarIcon', "Settings icon in the view bar.")); // {{SQL CARBON EDIT}} exporting to use it in getting started tour + private static readonly ACCOUNTS_ICON = registerIcon('accounts-view-bar-icon', Codicon.account, localize('accountsViewBarIcon', "Accounts icon in the view bar.")); + //#region IView readonly minimumWidth: number = 48; @@ -112,12 +114,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { private readonly accountsActivity: ICompositeActivity[] = []; - private readonly compositeActions = new Map(); + private readonly compositeActions = new Map(); private readonly viewContainerDisposables = new Map(); private readonly keyboardNavigationDisposables = this._register(new DisposableStore()); private readonly location = ViewContainerLocation.Sidebar; + private hasExtensionsRegistered: boolean = false; + + private readonly enabledViewContainersContextKeys: Map> = new Map>(); constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -134,14 +139,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); for (const cachedViewContainer of this.cachedViewContainers) { - if ( - environmentService.remoteAuthority || // In remote window, hide activity bar entries until registered - this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer) - ) { - cachedViewContainer.visible = false; - } + cachedViewContainer.visible = !this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer); } - this.compositeBar = this.createCompositeBar(); this.onDidRegisterViewContainers(this.getViewContainers()); @@ -163,53 +162,69 @@ export class ActivitybarPart extends Part implements IActivityBarService { icon: true, orientation: ActionsOrientation.VERTICAL, preventLoopNavigation: true, - openComposite: (compositeId: string) => this.viewsService.openViewContainer(compositeId, true), - getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction, - getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, - getOnCompositeClickAction: (compositeId: string) => new Action(compositeId, '', '', true, () => this.viewsService.isViewContainerVisible(compositeId) ? Promise.resolve(this.viewsService.closeViewContainer(compositeId)) : this.viewsService.openViewContainer(compositeId)), - getContextMenuActions: () => { - const actions = []; + openComposite: compositeId => this.viewsService.openViewContainer(compositeId, true), + getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction, + getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction, + getOnCompositeClickAction: compositeId => toAction({ id: compositeId, label: '', run: async () => this.viewsService.isViewContainerVisible(compositeId) ? this.viewsService.closeViewContainer(compositeId) : this.viewsService.openViewContainer(compositeId) }), + fillExtraContextMenuActions: actions => { // Home + const topActions: IAction[] = []; if (this.homeBarContainer) { - actions.push(new Action( - 'toggleHomeBarAction', - this.homeBarVisibilityPreference ? nls.localize('hideHomeBar', "Hide Home Button") : nls.localize('showHomeBar', "Show Home Button"), - undefined, - true, - async () => { this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference; } - )); + topActions.push({ + id: 'toggleHomeBarAction', + label: localize('homeButton', "Home Button"), + class: undefined, + tooltip: localize('homeButton', "Home Button"), + checked: this.homeBarVisibilityPreference, + enabled: true, + expanded: false, + run: async () => this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference, + dispose: () => { } + }); } // Menu const menuBarVisibility = getMenuBarVisibility(this.configurationService); if (menuBarVisibility === 'compact' || (menuBarVisibility === 'hidden' && isWeb)) { - actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu"))); + topActions.push({ + id: 'toggleMenuVisibility', + label: localize('menu', "Menu"), + class: undefined, + tooltip: localize('menu', "Menu"), + checked: menuBarVisibility === 'compact', + enabled: true, + expanded: false, + run: async () => this.layoutService.toggleMenuBar(), + dispose: () => { } + }); + } + + if (topActions.length) { + actions.unshift(...topActions, new Separator()); } // Accounts - actions.push(new Action( - 'toggleAccountsVisibility', - this.accountsVisibilityPreference ? nls.localize('hideAccounts', "Hide Accounts") : nls.localize('showAccounts', "Show Accounts"), - undefined, - true, - async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; } - )); + actions.push(new Separator()); + actions.push({ + id: 'toggleAccountsVisibility', + label: localize('accounts', "Accounts"), + class: undefined, + tooltip: localize('accounts', "Accounts"), + checked: this.accountsVisibilityPreference, + enabled: true, + expanded: false, + run: async () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference, + dispose: () => { } + }); + actions.push(new Separator()); // Toggle Sidebar actions.push(this.instantiationService.createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService))); // Toggle Activity Bar - actions.push(new Action( - ToggleActivityBarVisibilityAction.ID, - nls.localize('hideActivitBar', "Hide Activity Bar"), - undefined, - true, - async () => { this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)); } - )); - - return actions; + actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: async () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) })); }, getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.viewDescriptorService.getDefaultViewContainer(this.location)!.id, @@ -225,24 +240,20 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private getContextMenuActionsForComposite(compositeId: string): Action[] { - const actions = []; + private getContextMenuActionsForComposite(compositeId: string): IAction[] { + const actions: IAction[] = []; const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!; const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!; if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) { - actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => { - this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation); - })); + actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) })); } else { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.allViewDescriptors.length === 1) { const viewToReset = viewContainerModel.allViewDescriptors[0]; const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!; if (defaultContainer !== viewContainer) { - actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => { - this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer); - })); + actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) })); } } } @@ -280,7 +291,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>) { + private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>) { removed.filter(({ location }) => location === ViewContainerLocation.Sidebar).forEach(({ container }) => this.onDidDeregisterViewContainer(container)); this.onDidRegisterViewContainers(added.filter(({ location }) => location === ViewContainerLocation.Sidebar).map(({ container }) => container)); } @@ -312,7 +323,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private onDidRegisterExtensions(): void { - this.removeNotExistingComposites(); + this.hasExtensionsRegistered = true; + + // show/hide/remove composites + for (const { id } of this.cachedViewContainers) { + const viewContainer = this.getViewContainer(id); + if (viewContainer) { + this.showOrHideViewContainer(viewContainer); + } else { + if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) { + this.removeComposite(id); + } else { + this.hideComposite(id); + } + } + } + this.saveCachedViewContainers(); } @@ -321,10 +347,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (viewContainer) { // Update the composite bar by adding - this.compositeBar.addComposite(viewContainer); + this.addComposite(viewContainer); this.compositeBar.activateComposite(viewContainer.id); - if (viewContainer.hideIfEmpty) { + if (this.shouldBeHidden(viewContainer)) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.activeViewDescriptors.length === 0) { // Update the composite bar by hiding @@ -462,7 +488,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Home action bar const homeIndicator = this.environmentService.options?.homeIndicator; - if (homeIndicator) { + // TODO @sbatten remove the fake setting and associated code + if (homeIndicator && this.configurationService.getValue('window.showHomeIndicator')) { let codicon = iconRegistry.get(homeIndicator.icon); if (!codicon) { codicon = Codicon.code; @@ -558,14 +585,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { private createHomeBar(href: string, icon: Codicon): void { this.homeBarContainer = document.createElement('div'); - this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home")); + this.homeBarContainer.setAttribute('aria-label', localize('homeIndicator', "Home")); this.homeBarContainer.setAttribute('role', 'toolbar'); this.homeBarContainer.classList.add('home-bar'); this.homeBar = this._register(new ActionBar(this.homeBarContainer, { - actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)), + actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)), orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('home', "Home"), + ariaLabel: localize('home', "Home"), animated: false, preventLoopNavigation: true, ignoreOrientationForPreviousAndNextKey: true @@ -577,7 +604,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.homeBar.push(this._register(new ActivityAction({ id: 'workbench.actions.home', - name: nls.localize('home', "Home"), + name: localize('home', "Home"), cssClass: icon.classNames }))); @@ -589,17 +616,17 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.globalActivityActionBar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === 'workbench.actions.manage') { - return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)); } if (action.id === 'workbench.actions.accounts') { - return this.instantiationService.createInstance(AccountsActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)); + return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)); } throw new Error(`No view item for action '${action.id}'`); }, orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('manage', "Manage"), + ariaLabel: localize('manage', "Manage"), animated: false, preventLoopNavigation: true, ignoreOrientationForPreviousAndNextKey: true @@ -607,15 +634,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.globalActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.manage', - name: nls.localize('manage', "Manage"), - cssClass: ThemeIcon.asClassName(settingsViewBarIcon) + name: localize('manage', "Manage"), + cssClass: ThemeIcon.asClassName(ActivitybarPart.GEAR_ICON) })); if (this.accountsVisibilityPreference) { this.accountsActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), - cssClass: ThemeIcon.asClassName(accountsViewBarIcon) + name: localize('accounts', "Accounts"), + cssClass: ThemeIcon.asClassName(ActivitybarPart.ACCOUNTS_ICON) })); this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); @@ -632,7 +659,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { } else { this.accountsActivityAction = this._register(new ActivityAction({ id: 'workbench.actions.accounts', - name: nls.localize('accounts', "Accounts"), + name: localize('accounts', "Accounts"), cssClass: Codicon.account.classNames })); this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX }); @@ -642,15 +669,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.updateGlobalActivity(ACCOUNTS_ACTIVITY_ID); } - private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } { + private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction; } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { const viewContainer = this.getViewContainer(compositeId); if (viewContainer) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); compositeActions = { - activityAction: this.instantiationService.createInstance(ViewContainerActivityAction, this.toActivity(viewContainer, viewContainerModel)), - pinnedAction: new ToggleCompositePinnedAction(viewContainer, this.compositeBar) + activityAction: this.instantiationService.createInstance(ViewContainerActivityAction, this.toActivity(viewContainerModel)), + pinnedAction: new ToggleCompositePinnedAction(this.toActivity(viewContainerModel), this.compositeBar) }; } else { const cachedComposite = this.cachedViewContainers.filter(c => c.id === compositeId)[0]; @@ -668,32 +695,27 @@ export class ActivitybarPart extends Part implements IActivityBarService { private onDidRegisterViewContainers(viewContainers: ReadonlyArray): void { for (const viewContainer of viewContainers) { + this.addComposite(viewContainer); + + // Pin it by default if it is new const cachedViewContainer = this.cachedViewContainers.filter(({ id }) => id === viewContainer.id)[0]; - const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location); - const isActive = visibleViewContainer?.id === viewContainer.id; - - if (isActive || !this.shouldBeHidden(viewContainer.id, cachedViewContainer)) { - this.compositeBar.addComposite(viewContainer); - - // Pin it by default if it is new - if (!cachedViewContainer) { - this.compositeBar.pin(viewContainer.id); - } - - if (isActive) { - this.compositeBar.activateComposite(viewContainer.id); - } + if (!cachedViewContainer) { + this.compositeBar.pin(viewContainer.id); + } + + // Active + const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location); + if (visibleViewContainer?.id === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); } - } - for (const viewContainer of viewContainers) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); this.updateActivity(viewContainer, viewContainerModel); - this.onDidChangeActiveViews(viewContainer, viewContainerModel); + this.showOrHideViewContainer(viewContainer); const disposables = new DisposableStore(); disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel))); - disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel))); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.showOrHideViewContainer(viewContainer))); this.viewContainerDisposables.set(viewContainer.id, disposables); } @@ -710,7 +732,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private updateActivity(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { - const activity: IActivity = this.toActivity(viewContainer, viewContainerModel); + const activity: IActivity = this.toActivity(viewContainerModel); const { activityAction, pinnedAction } = this.getCompositeActions(viewContainer.id); activityAction.updateActivity(activity); @@ -721,8 +743,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.saveCachedViewContainers(); } - private toActivity({ id, focusCommand }: ViewContainer, { icon, title: name }: IViewContainerModel): IActivity { - return ActivitybarPart.toActivity(id, name, icon, focusCommand?.id || id); + private toActivity(viewContainerModel: IViewContainerModel): IActivity { + return ActivitybarPart.toActivity(viewContainerModel.viewContainer.id, viewContainerModel.title, viewContainerModel.icon, viewContainerModel.keybindingId); } private static toActivity(id: string, name: string, icon: URI | ThemeIcon | undefined, keybindingId: string | undefined): IActivity { @@ -730,12 +752,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { let iconUrl: URI | undefined = undefined; if (URI.isUri(icon)) { iconUrl = icon; - cssClass = `activity-${id.replace(/\./g, '-')}`; + const cssUrl = asCSSUrl(icon); + const hash = new StringSHA1(); + hash.update(cssUrl); + cssClass = `activity-${id.replace(/\./g, '-')}-${hash.digest()}`; const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask: ${cssUrl} no-repeat 50% 50%; mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask: ${cssUrl} no-repeat 50% 50%; -webkit-mask-size: 24px; `); } else if (ThemeIcon.isThemeIcon(icon)) { @@ -745,36 +770,54 @@ export class ActivitybarPart extends Part implements IActivityBarService { return { id, name, cssClass, iconUrl, keybindingId }; } - private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { - if (viewContainerModel.activeViewDescriptors.length) { - this.compositeBar.addComposite(viewContainer); - } else if (viewContainer.hideIfEmpty) { + private showOrHideViewContainer(viewContainer: ViewContainer): void { + let contextKey = this.enabledViewContainersContextKeys.get(viewContainer.id); + if (!contextKey) { + contextKey = this.contextKeyService.createKey(getEnabledViewContainerContextKey(viewContainer.id), false); + this.enabledViewContainersContextKeys.set(viewContainer.id, contextKey); + } + if (this.shouldBeHidden(viewContainer)) { + contextKey.set(false); this.hideComposite(viewContainer.id); + } else { + contextKey.set(true); + this.addComposite(viewContainer); } } - private shouldBeHidden(viewContainerId: string, cachedViewContainer?: ICachedViewContainer): boolean { - const viewContainer = this.getViewContainer(viewContainerId); - if (!viewContainer || !viewContainer.hideIfEmpty) { - return false; - } + private shouldBeHidden(viewContainerOrId: string | ViewContainer, cachedViewContainer?: ICachedViewContainer): boolean { + const viewContainer = isString(viewContainerOrId) ? this.getViewContainer(viewContainerOrId) : viewContainerOrId; + const viewContainerId = isString(viewContainerOrId) ? viewContainerOrId : viewContainerOrId.id; - return cachedViewContainer?.views && cachedViewContainer.views.length - ? cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))) - : viewContainerId === TEST_VIEW_CONTAINER_ID /* Hide Test view container for the first time or it had no views registered before */; - } - - private removeNotExistingComposites(): void { - const viewContainers = this.getViewContainers(); - for (const { id } of this.cachedViewContainers) { - if (viewContainers.every(viewContainer => viewContainer.id !== id)) { - if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) { - this.removeComposite(id); - } else { - this.hideComposite(id); + if (viewContainer) { + if (viewContainer.hideIfEmpty) { + if (this.viewDescriptorService.getViewContainerModel(viewContainer).activeViewDescriptors.length > 0) { + return false; } + } else { + return false; } } + + // Check cache only if extensions are not yet registered and current window is not native (desktop) remote connection window + if (!this.hasExtensionsRegistered && !(this.environmentService.remoteAuthority && isNative)) { + cachedViewContainer = cachedViewContainer || this.cachedViewContainers.find(({ id }) => id === viewContainerId); + + // Show builtin ViewContainer if not registered yet + if (!viewContainer && cachedViewContainer?.isBuiltin) { + return false; + } + + if (cachedViewContainer?.views?.length) { + return cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))); + } + } + + return true; + } + + private addComposite(viewContainer: ViewContainer): void { + this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); } private hideComposite(compositeId: string): void { @@ -866,7 +909,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { private getViewContainer(id: string): ViewContainer | undefined { const viewContainer = this.viewDescriptorService.getViewContainerById(id); - return viewContainer && this.viewDescriptorService.getViewContainerLocation(viewContainer) === this.location ? viewContainer : undefined; } @@ -920,22 +962,22 @@ export class ActivitybarPart extends Part implements IActivityBarService { const viewContainer = this.getViewContainer(compositeItem.id); if (viewContainer) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); - const views: { when: string | undefined }[] = []; + const views: { when: string | undefined; }[] = []; for (const { when } of viewContainerModel.allViewDescriptors) { views.push({ when: when ? when.serialize() : undefined }); } - const cacheIcon = URI.isUri(viewContainerModel.icon) ? viewContainerModel.icon.scheme === Schemas.file : true; state.push({ id: compositeItem.id, name: viewContainerModel.title, - icon: cacheIcon ? viewContainerModel.icon : undefined, + icon: URI.isUri(viewContainerModel.icon) && this.environmentService.remoteAuthority && isNative ? undefined : viewContainerModel.icon, /* Donot cache uri icons in desktop with remote connection */ views, pinned: compositeItem.pinned, order: compositeItem.order, - visible: compositeItem.visible + visible: compositeItem.visible, + isBuiltin: !viewContainer.extensionId }); } else { - state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false }); + state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false, isBuiltin: false }); } } @@ -950,9 +992,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { const cachedViewContainer = this._cachedViewContainers.filter(cached => cached.id === placeholderViewContainer.id)[0]; if (cachedViewContainer) { cachedViewContainer.name = placeholderViewContainer.name; - cachedViewContainer.icon = placeholderViewContainer.themeIcon ? ThemeIcon.revive(placeholderViewContainer.themeIcon) : + cachedViewContainer.icon = placeholderViewContainer.themeIcon ? placeholderViewContainer.themeIcon : placeholderViewContainer.iconUrl ? URI.revive(placeholderViewContainer.iconUrl) : undefined; cachedViewContainer.views = placeholderViewContainer.views; + cachedViewContainer.isBuiltin = placeholderViewContainer.isBuiltin; } } } @@ -968,11 +1011,12 @@ export class ActivitybarPart extends Part implements IActivityBarService { order }))); - this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views }) => ({ + this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views, isBuiltin }) => ({ id, iconUrl: URI.isUri(icon) ? icon : undefined, themeIcon: ThemeIcon.isThemeIcon(icon) ? icon : undefined, name, + isBuiltin, views }))); } @@ -1069,7 +1113,7 @@ class FocusActivityBarAction extends Action2 { constructor() { super({ id: 'workbench.action.focusActivityBar', - title: { value: nls.localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, + title: { value: localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' }, category: CATEGORIES.View, f1: true }); diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 61850a6861..df0a62bf31 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { illegalArgument } from 'vs/base/common/errors'; import * as arrays from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -149,10 +149,10 @@ export interface ICompositeBarOptions { readonly preventLoopNavigation?: boolean; getActivityAction: (compositeId: string) => ActivityAction; - getCompositePinnedAction: (compositeId: string) => Action; - getOnCompositeClickAction: (compositeId: string) => Action; - getContextMenuActions: () => Action[]; - getContextMenuActionsForComposite: (compositeId: string) => Action[]; + getCompositePinnedAction: (compositeId: string) => IAction; + getOnCompositeClickAction: (compositeId: string) => IAction; + fillExtraContextMenuActions: (actions: IAction[]) => void; + getContextMenuActionsForComposite: (compositeId: string) => IAction[]; openComposite: (compositeId: string) => Promise; getDefaultCompositeId: () => string; hidePart: () => void; @@ -208,15 +208,15 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { - actionViewItemProvider: (action: IAction) => { + actionViewItemProvider: action => { if (action instanceof CompositeOverflowActivityAction) { return this.compositeOverflowActionViewItem; } const item = this.model.findItem(action.id); return item && this.instantiationService.createInstance( CompositeActionViewItem, action as ActivityAction, item.pinnedAction, - (compositeId: string) => this.options.getContextMenuActionsForComposite(compositeId), - () => this.getContextMenuActions() as Action[], + compositeId => this.options.getContextMenuActionsForComposite(compositeId), + () => this.getContextMenuActions(), this.options.colors, this.options.icon, this.options.dndHandler, @@ -319,7 +319,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this.updateCompositeSwitcher(); } - addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number }): void { + addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number; }): void { // Add to the model if (this.model.add(id, name, order, requestedIndex)) { this.computeSizes([this.model.findItem(id)]); @@ -596,7 +596,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this.compositeOverflowAction, () => this.getOverflowingComposites(), () => this.model.activeItem ? this.model.activeItem.id : undefined, - (compositeId: string) => { + compositeId => { const item = this.model.findItem(compositeId); return item?.activity[0]?.badge; }, @@ -610,7 +610,7 @@ export class CompositeBar extends Widget implements ICompositeBar { this._onDidChange.fire(); } - private getOverflowingComposites(): { id: string, name?: string }[] { + private getOverflowingComposites(): { id: string, name?: string; }[] { let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id); // Show the active composite even if it is not pinned @@ -631,9 +631,9 @@ export class CompositeBar extends Widget implements ICompositeBar { }); } - private getContextMenuActions(): IAction[] { + getContextMenuActions(): IAction[] { const actions: IAction[] = this.model.visibleItems - .map(({ id, name, activityAction }) => ({ + .map(({ id, name, activityAction }) => (toAction({ id, label: this.getAction(id).label || name || id, checked: this.isPinned(id), @@ -645,19 +645,17 @@ export class CompositeBar extends Widget implements ICompositeBar { this.pin(id, true); } } - })); - const otherActions = this.options.getContextMenuActions(); - if (otherActions.length) { - actions.push(new Separator()); - actions.push(...otherActions); - } + }))); + + this.options.fillExtraContextMenuActions(actions); + return actions; } } interface ICompositeBarModelItem extends ICompositeBarItem { activityAction: ActivityAction; - pinnedAction: Action; + pinnedAction: IAction; activity: ICompositeActivity[]; } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index ee0b79cf97..cefb65ae51 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Action, Separator } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -374,14 +374,14 @@ export class CompositeOverflowActivityAction extends ActivityAction { } export class CompositeOverflowActivityActionViewItem extends ActivityActionViewItem { - private actions: Action[] = []; + private actions: IAction[] = []; constructor( action: ActivityAction, private getOverflowingComposites: () => { id: string, name?: string }[], private getActiveCompositeId: () => string | undefined, private getBadge: (compositeId: string) => IBadge, - private getCompositeOpenAction: (compositeId: string) => Action, + private getCompositeOpenAction: (compositeId: string) => IAction, colors: (theme: IColorTheme) => ICompositeBarColors, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService @@ -404,7 +404,7 @@ export class CompositeOverflowActivityActionViewItem extends ActivityActionViewI }); } - private getActions(): Action[] { + private getActions(): IAction[] { return this.getOverflowingComposites().map(composite => { const action = this.getCompositeOpenAction(composite.id); action.checked = this.getActiveCompositeId() === action.id; @@ -457,9 +457,9 @@ export class CompositeActionViewItem extends ActivityActionViewItem { constructor( private compositeActivityAction: ActivityAction, - private toggleCompositePinnedAction: Action, - private compositeContextMenuActionsProvider: (compositeId: string) => ReadonlyArray, - private contextMenuActionsProvider: () => ReadonlyArray, + private toggleCompositePinnedAction: IAction, + private compositeContextMenuActionsProvider: (compositeId: string) => IAction[], + private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, icon: boolean, private dndHandler: ICompositeDragAndDrop, @@ -500,9 +500,14 @@ export class CompositeActionViewItem extends ActivityActionViewItem { return this.compositeActivity; } - private getActivtyName(): string { + private getActivtyName(skipKeybinding = false): string { + let name = this.compositeActivityAction.activity.name; + if (skipKeybinding) { + return name; + } + const keybinding = this.compositeActivityAction.activity.keybindingId ? this.keybindingService.lookupKeybinding(this.compositeActivityAction.activity.keybindingId) : null; - return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding.getLabel()) : this.compositeActivityAction.activity.name; + return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", name, keybinding.getLabel()) : name; } render(container: HTMLElement): void { @@ -601,7 +606,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem { } private showContextMenu(container: HTMLElement): void { - const actions: Action[] = [this.toggleCompositePinnedAction]; + const actions: IAction[] = [this.toggleCompositePinnedAction]; const compositeContextMenuActions = this.compositeContextMenuActionsProvider(this.activity.id); if (compositeContextMenuActions.length) { @@ -615,10 +620,10 @@ export class CompositeActionViewItem extends ActivityActionViewItem { const isPinned = this.compositeBar.isPinned(this.activity.id); if (isPinned) { - this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide"); + this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide '{0}'", this.getActivtyName(true)); this.toggleCompositePinnedAction.checked = false; } else { - this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep"); + this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep '{0}'", this.getActivtyName(true)); } const otherActions = this.contextMenuActionsProvider(); diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 6a1aa137ed..f7f8484a48 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -96,7 +96,7 @@ export abstract class CompositePart extends Part { protected openComposite(id: string, focus?: boolean): Composite | undefined { // Check if composite already visible and just focus in that case - if (this.activeComposite && this.activeComposite.getId() === id) { + if (this.activeComposite?.getId() === id) { if (focus) { this.activeComposite.focus(); } @@ -137,7 +137,7 @@ export abstract class CompositePart extends Part { } // Check if composite already visible and just focus in that case - if (this.activeComposite && this.activeComposite.getId() === composite.getId()) { + if (this.activeComposite?.getId() === composite.getId()) { if (focus) { composite.focus(); } @@ -290,7 +290,7 @@ export abstract class CompositePart extends Part { } // Active Composite - if (this.activeComposite && this.activeComposite.getId() === compositeId) { + if (this.activeComposite?.getId() === compositeId) { // Actions const actionsBinding = this.collectCompositeActions(this.activeComposite); this.mapActionsBindingToComposite.set(this.activeComposite.getId(), actionsBinding); @@ -327,10 +327,6 @@ export abstract class CompositePart extends Part { const primaryActions: IAction[] = composite?.getActions().slice(0) || []; const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || []; - // From Part - primaryActions.push(...this.getActions()); - secondaryActions.push(...this.getSecondaryActions()); - // Update context const toolBar = assertIsDefined(this.toolBar); toolBar.context = this.actionsContextProvider(); @@ -471,14 +467,6 @@ export abstract class CompositePart extends Part { return compositeItem ? compositeItem.progress : undefined; } - protected getActions(): ReadonlyArray { - return []; - } - - protected getSecondaryActions(): ReadonlyArray { - return []; - } - protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment { return AnchorAlignment.RIGHT; } @@ -496,7 +484,7 @@ export abstract class CompositePart extends Part { } protected removeComposite(compositeId: string): boolean { - if (this.activeComposite && this.activeComposite.getId() === compositeId) { + if (this.activeComposite?.getId() === compositeId) { return false; // do not remove active composite } diff --git a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts index 95b933fc6b..3e04e3fb87 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts @@ -19,9 +19,9 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Disposable } from 'vs/base/common/lifecycle'; export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution { - private impl: IDialogHandler; + private readonly model: IDialogsModel; + private readonly impl: IDialogHandler; - private model: IDialogsModel; private currentDialog: IDialogViewItem | undefined; constructor( diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index abb5e0ba36..e7c9b3a6ef 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -87,7 +87,7 @@ export class BrowserDialogHandler implements IDialogHandler { type, keyEventProcessor: (event: StandardKeyboardEvent) => { const resolved = this.keybindingService.softDispatch(event, this.layoutService.container); - if (resolved && resolved.commandId) { + if (resolved?.commandId) { if (BrowserDialogHandler.ALLOWABLE_COMMANDS.indexOf(resolved.commandId) === -1) { EventHelper.stop(event, true); } diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index a618350a32..bfb04abfed 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -139,7 +139,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); size(binaryContainer, dimension.width, dimension.height); scrollbar.scanDomNode(); - if (this.resourceViewerContext && this.resourceViewerContext.layout) { + if (typeof this.resourceViewerContext?.layout === 'function') { this.resourceViewerContext.layout(dimension); } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 7a3f87ed80..72395115e0 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -6,19 +6,13 @@ import * as dom from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { tail } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { Range } from 'vs/editor/common/core/range'; -import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; -import { SymbolKinds } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -28,35 +22,92 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { IListService, WorkbenchDataTree, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry'; import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; -import { BreadcrumbsPicker, createBreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; +import { BreadcrumbsModel, FileElement, OutlineElement2 } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; import { IEditorPartOptions, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; -import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { IOutline } from 'vs/workbench/services/outline/browser/outline'; -class Item extends BreadcrumbsItem { +class OutlineItem extends BreadcrumbsItem { private readonly _disposables = new DisposableStore(); constructor( - readonly element: BreadcrumbElement, + readonly model: BreadcrumbsModel, + readonly element: OutlineElement2, + readonly options: IBreadcrumbsControlOptions + ) { + super(); + } + + dispose(): void { + this._disposables.dispose(); + } + + equals(other: BreadcrumbsItem): boolean { + if (!(other instanceof OutlineItem)) { + return false; + } + return this.element === other.element && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons; + } + + render(container: HTMLElement): void { + const { element, outline } = this.element; + + if (element === outline) { + const element = dom.$('span', undefined, '…'); + container.appendChild(element); + return; + } + + const templateId = outline.config.delegate.getTemplateId(element); + const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId); + if (!renderer) { + container.innerText = '<>'; + return; + } + + const template = renderer.renderTemplate(container); + renderer.renderElement(>{ + element, + children: [], + depth: 0, + visibleChildrenCount: 0, + visibleChildIndex: 0, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined + }, 0, template, undefined); + + this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); })); + } + +} + +class FileItem extends BreadcrumbsItem { + + private readonly _disposables = new DisposableStore(); + + constructor( + readonly model: BreadcrumbsModel, + readonly element: FileElement, readonly options: IBreadcrumbsControlOptions, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { @@ -68,59 +119,26 @@ class Item extends BreadcrumbsItem { } equals(other: BreadcrumbsItem): boolean { - if (!(other instanceof Item)) { + if (!(other instanceof FileItem)) { return false; } - if (this.element instanceof FileElement && other.element instanceof FileElement) { - return (extUri.isEqual(this.element.uri, other.element.uri) && - this.options.showFileIcons === other.options.showFileIcons && - this.options.showSymbolIcons === other.options.showSymbolIcons); - } - if (this.element instanceof TreeElement && other.element instanceof TreeElement) { - return this.element.id === other.element.id; - } - return false; + return (extUri.isEqual(this.element.uri, other.element.uri) && + this.options.showFileIcons === other.options.showFileIcons && + this.options.showSymbolIcons === other.options.showSymbolIcons); + } render(container: HTMLElement): void { - if (this.element instanceof FileElement) { - // file/folder - let label = this._instantiationService.createInstance(ResourceLabel, container, {}); - label.element.setFile(this.element.uri, { - hidePath: true, - hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, - fileKind: this.element.kind, - fileDecorations: { colors: this.options.showDecorationColors, badges: false }, - }); - container.classList.add(FileKind[this.element.kind].toLowerCase()); - this._disposables.add(label); - - } else if (this.element instanceof OutlineModel) { - // has outline element but not in one - let label = document.createElement('div'); - label.innerText = '\u2026'; - label.className = 'hint-more'; - container.appendChild(label); - - } else if (this.element instanceof OutlineGroup) { - // provider - let label = new IconLabel(container); - label.setLabel(this.element.label); - this._disposables.add(label); - - } else if (this.element instanceof OutlineElement) { - // symbol - if (this.options.showSymbolIcons) { - let icon = document.createElement('div'); - icon.className = SymbolKinds.toCssClassName(this.element.symbol.kind); - container.appendChild(icon); - container.classList.add('shows-symbol-icon'); - } - let label = new IconLabel(container); - let title = this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE'); - label.setLabel(title); - this._disposables.add(label); - } + // file/folder + let label = this._instantiationService.createInstance(ResourceLabel, container, {}); + label.element.setFile(this.element.uri, { + hidePath: true, + hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, + fileKind: this.element.kind, + fileDecorations: { colors: this.options.showDecorationColors, badges: false }, + }); + container.classList.add(FileKind[this.element.kind].toLowerCase()); + this._disposables.add(label); } } @@ -170,26 +188,23 @@ export class BreadcrumbsControl { private readonly _editorGroup: IEditorGroupView, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextViewService private readonly _contextViewService: IContextViewService, - @IEditorService private readonly _editorService: IEditorService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @IFileService private readonly _fileService: IFileService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, + @IConfigurationService configurationService: IConfigurationService, @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, ) { this.domNode = document.createElement('div'); this.domNode.classList.add('breadcrumbs-control'); dom.append(container, this.domNode); - this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService); - this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(_configurationService); - this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(_configurationService); + this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService); + this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService); + this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService); const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default'; this._widget = new BreadcrumbsWidget(this.domNode, BreadcrumbsControl.SCROLLBAR_SIZES[sizing]); @@ -256,14 +271,11 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible.set(true); this._ckBreadcrumbsPossible.set(true); - const editor = this._getActiveCodeEditor(); - const model = new EditorBreadcrumbsModel( + const model = this._instantiationService.createInstance(BreadcrumbsModel, fileInfoUri ?? uri, - uri, editor, - this._configurationService, - this._textResourceConfigurationService, - this._workspaceService + this._editorGroup.activeEditorPane ); + this.domNode.classList.toggle('relative-path', model.isRelative()); this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\'); @@ -274,7 +286,7 @@ export class BreadcrumbsControl { showFileIcons: this._options.showFileIcons && showIcons, showSymbolIcons: this._options.showSymbolIcons && showIcons }; - const items = model.getElements().map(element => new Item(element, options, this._instantiationService)); + const items = model.getElements().map(element => element instanceof FileElement ? new FileItem(model, element, options, this._instantiationService) : new OutlineItem(model, element, options)); this._widget.setItems(items); this._widget.reveal(items[items.length - 1]); }; @@ -298,7 +310,7 @@ export class BreadcrumbsControl { this._breadcrumbsDisposables.add({ dispose: () => { if (this._breadcrumbsPickerShowing) { - this._contextViewService.hideContextView(this); + this._contextViewService.hideContextView({ source: this }); } } }); @@ -306,20 +318,6 @@ export class BreadcrumbsControl { return true; } - private _getActiveCodeEditor(): ICodeEditor | undefined { - if (!this._editorGroup.activeEditorPane) { - return undefined; - } - let control = this._editorGroup.activeEditorPane.getControl(); - let editor: ICodeEditor | undefined; - if (isCodeEditor(control)) { - editor = control as ICodeEditor; - } else if (isDiffEditor(control)) { - editor = control.getModifiedEditor(); - } - return editor; - } - private _onFocusEvent(event: IBreadcrumbsItemEvent): void { if (event.item && this._breadcrumbsPickerShowing) { this._breadcrumbsPickerIgnoreOnceItem = undefined; @@ -339,13 +337,12 @@ export class BreadcrumbsControl { return; } - const { element } = event.item as Item; + const { element } = event.item as FileItem | OutlineItem; this._editorGroup.focus(); - type BreadcrumbSelectClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this._telemetryService.publicLog2<{ type: string }, BreadcrumbSelectClassification>('breadcrumbs/select', { type: element instanceof TreeElement ? 'symbol' : 'file' }); + type BreadcrumbSelect = { type: string }; + type BreadcrumbSelectClassification = { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; + this._telemetryService.publicLog2('breadcrumbs/select', { type: event.item instanceof OutlineItem ? 'symbol' : 'file' }); const group = this._getEditorGroup(event.payload); if (group !== undefined) { @@ -360,64 +357,31 @@ export class BreadcrumbsControl { // using quick pick this._widget.setFocused(undefined); this._widget.setSelection(undefined); - this._quickInputService.quickAccess.show(element instanceof TreeElement ? '@' : ''); + this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : ''); return; } // show picker let picker: BreadcrumbsPicker; let pickerAnchor: { x: number; y: number }; - let editor = this._getActiveCodeEditor(); - let editorDecorations: string[] = []; - let editorViewState: ICodeEditorViewState | undefined; + + interface IHideData { didPick?: boolean, source?: BreadcrumbsControl } this._contextViewService.showContextView({ render: (parent: HTMLElement) => { - picker = createBreadcrumbsPicker(this._instantiationService, parent, element); - let selectListener = picker.onDidPickElement(data => { - if (data.target) { - editorViewState = undefined; - } - this._contextViewService.hideContextView(this); + if (event.item instanceof FileItem) { + picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource); + } else if (event.item instanceof OutlineItem) { + picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource); + } - const group = (picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).metaKey) || (!picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).altKey) - ? SIDE_GROUP - : ACTIVE_GROUP; - - this._revealInEditor(event, data.target, group, (data.browserEvent as MouseEvent).button === 1); - /* __GDPR__ - "breadcrumbs/open" : { - "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('breadcrumbs/open', { type: !data ? 'nothing' : data.target instanceof TreeElement ? 'symbol' : 'file' }); - }); - let focusListener = picker.onDidFocusElement(data => { - if (!editor || !(data.target instanceof OutlineElement)) { - return; - } - if (!editorViewState) { - editorViewState = withNullAsUndefined(editor.saveViewState()); - } - const { symbol } = data.target; - editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); - editorDecorations = editor.deltaDecorations(editorDecorations, [{ - range: symbol.range, - options: { - className: 'rangeHighlight', - isWholeLine: true - } - }]); - }); - - let zoomListener = onDidChangeZoomLevel(() => { - this._contextViewService.hideContextView(this); - }); + let selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true })); + let zoomListener = onDidChangeZoomLevel(() => this._contextViewService.hideContextView({ source: this })); let focusTracker = dom.trackFocus(parent); let blurListener = focusTracker.onDidBlur(() => { this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined; - this._contextViewService.hideContextView(this); + this._contextViewService.hideContextView({ source: this }); }); this._breadcrumbsPickerShowing = true; @@ -426,7 +390,6 @@ export class BreadcrumbsControl { return combinedDisposable( picker, selectListener, - focusListener, zoomListener, focusTracker, blurListener @@ -465,19 +428,17 @@ export class BreadcrumbsControl { } return pickerAnchor; }, - onHide: (data) => { - if (editor) { - editor.deltaDecorations(editorDecorations, []); - if (editorViewState) { - editor.restoreViewState(editorViewState); - } + onHide: (data?: IHideData) => { + if (!data?.didPick) { + picker.restoreViewState(); } this._breadcrumbsPickerShowing = false; this._updateCkBreadcrumbsActive(); - if (data === this) { + if (data?.source === this) { this._widget.setFocused(undefined); this._widget.setSelection(undefined); } + picker.dispose(); } }); } @@ -487,11 +448,11 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive.set(value); } - private _revealInEditor(event: IBreadcrumbsItemEvent, element: BreadcrumbElement, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): void { + private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise { + if (element instanceof FileElement) { if (element.kind === FileKind.FILE) { - // open file in any editor - this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); + await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); } else { // show next picker let items = this._widget.getItems(); @@ -499,20 +460,8 @@ export class BreadcrumbsControl { this._widget.setFocused(items[idx + 1]); this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); } - - } else if (element instanceof OutlineElement) { - // open symbol in code editor - const model = OutlineModel.get(element); - if (model) { - this._codeEditorService.openCodeEditor({ - resource: model.uri, - options: { - selection: Range.collapseToStart(element.symbol.selectionRange), - selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, - pinned - } - }, withUndefinedAsNull(this._getActiveCodeEditor()), group === SIDE_GROUP); - } + } else { + element.outline.reveal(element, { pinned }, group === SIDE_GROUP); } } @@ -746,29 +695,29 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler(accessor) { const editors = accessor.get(IEditorService); const lists = accessor.get(IListService); - const element = lists.lastFocusedList ? lists.lastFocusedList.getFocus()[0] : undefined; - if (element instanceof OutlineElement) { - const outlineElement = OutlineModel.get(element); - if (!outlineElement) { - return undefined; - } - // open symbol in editor - return editors.openEditor({ - resource: outlineElement.uri, - options: { selection: Range.collapseToStart(element.symbol.selectionRange), pinned: true } - }, SIDE_GROUP); + const tree = lists.lastFocusedList; + if (!(tree instanceof WorkbenchDataTree)) { + return; + } - } else if (element && URI.isUri(element.resource)) { - // open file in editor + const element = tree.getFocus()[0]; + + if (URI.isUri((element)?.resource)) { + // IFileStat: open file in editor return editors.openEditor({ - resource: element.resource, + resource: (element).resource, options: { pinned: true } }, SIDE_GROUP); + } - } else { - // ignore - return undefined; + // IOutline: check if this the outline and iff so reveal element + const input = tree.getInput(); + if (input && typeof (>input).outlineKind === 'string') { + return (>input).reveal(element, { + pinned: true, + preserveFocus: false + }, true); } } }); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index ec183398b2..ba59e8c5c0 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -3,27 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; -import { TimeoutTimer } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isEqual, dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IPosition } from 'vs/editor/common/core/position'; -import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; -import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { FileKind } from 'vs/platform/files/common/files'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree'; -import { ITextModel } from 'vs/editor/common/model'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { IEditorPane } from 'vs/workbench/common/editor'; export class FileElement { constructor( @@ -32,11 +25,16 @@ export class FileElement { ) { } } -export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement; - type FileInfo = { path: FileElement[], folder?: IWorkspaceFolder }; -export class EditorBreadcrumbsModel { +export class OutlineElement2 { + constructor( + readonly element: IOutline | any, + readonly outline: IOutline + ) { } +} + +export class BreadcrumbsModel { private readonly _disposables = new DisposableStore(); private readonly _fileInfo: FileInfo; @@ -45,28 +43,31 @@ export class EditorBreadcrumbsModel { private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>; private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>; - private _outlineElements: Array = []; - private _outlineDisposables = new DisposableStore(); + private readonly _currentOutline = new MutableDisposable>(); + private readonly _outlineDisposables = new DisposableStore(); private readonly _onDidUpdate = new Emitter(); readonly onDidUpdate: Event = this._onDidUpdate.event; constructor( - fileInfoUri: URI, - private readonly _uri: URI, - private readonly _editor: ICodeEditor | undefined, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, - @IWorkspaceContextService workspaceService: IWorkspaceContextService, + readonly resource: URI, + editor: IEditorPane | undefined, + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IOutlineService private readonly _outlineService: IOutlineService, ) { - this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(_configurationService); - this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(_configurationService); - this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(_configurationService); + this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(configurationService); + this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService); + this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService); this._disposables.add(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this))); this._disposables.add(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this))); - this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(fileInfoUri, workspaceService); - this._bindToEditor(); + this._fileInfo = this._initFilePathInfo(resource); + + if (editor) { + this._bindToEditor(editor); + this._disposables.add(_outlineService.onDidChange(() => this._bindToEditor(editor))); + } this._onDidUpdate.fire(this); } @@ -74,6 +75,7 @@ export class EditorBreadcrumbsModel { this._cfgEnabled.dispose(); this._cfgFilePath.dispose(); this._cfgSymbolPath.dispose(); + this._currentOutline.dispose(); this._outlineDisposables.dispose(); this._disposables.dispose(); this._onDidUpdate.dispose(); @@ -83,8 +85,8 @@ export class EditorBreadcrumbsModel { return Boolean(this._fileInfo.folder); } - getElements(): ReadonlyArray { - let result: BreadcrumbElement[] = []; + getElements(): ReadonlyArray { + let result: (FileElement | OutlineElement2)[] = []; // file path elements if (this._cfgFilePath.getValue() === 'on') { @@ -93,17 +95,27 @@ export class EditorBreadcrumbsModel { result = result.concat(this._fileInfo.path.slice(-1)); } - // symbol path elements - if (this._cfgSymbolPath.getValue() === 'on') { - result = result.concat(this._outlineElements); - } else if (this._cfgSymbolPath.getValue() === 'last' && this._outlineElements.length > 0) { - result = result.concat(this._outlineElements.slice(-1)); + if (this._cfgSymbolPath.getValue() === 'off') { + return result; + } + + if (!this._currentOutline.value) { + return result; + } + + const breadcrumbsElements = this._currentOutline.value.config.breadcrumbsDataSource.getBreadcrumbElements(); + for (let i = this._cfgSymbolPath.getValue() === 'last' ? breadcrumbsElements.length - 1 : 0; i < breadcrumbsElements.length; i++) { + result.push(new OutlineElement2(breadcrumbsElements[i], this._currentOutline.value)); + } + + if (breadcrumbsElements.length === 0 && !this._currentOutline.value.isEmpty) { + result.push(new OutlineElement2(this._currentOutline.value, this._currentOutline.value)); } return result; } - private static _initFilePathInfo(uri: URI, workspaceService: IWorkspaceContextService): FileInfo { + private _initFilePathInfo(uri: URI): FileInfo { if (uri.scheme === Schemas.untitled) { return { @@ -113,7 +125,7 @@ export class EditorBreadcrumbsModel { } let info: FileInfo = { - folder: withNullAsUndefined(workspaceService.getWorkspaceFolder(uri)), + folder: withNullAsUndefined(this._workspaceService.getWorkspaceFolder(uri)), path: [] }; @@ -130,181 +142,33 @@ export class EditorBreadcrumbsModel { } } - if (info.folder && workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + if (info.folder && this._workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) { info.path.unshift(new FileElement(info.folder.uri, FileKind.ROOT_FOLDER)); } return info; } - private _bindToEditor(): void { - if (!this._editor) { - return; - } - // update as language, model, providers changes - this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline())); - this._disposables.add(this._editor.onDidChangeModel(_ => this._updateOutline())); - this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._updateOutline())); - - // update when config changes (re-render) - this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (!this._cfgEnabled.getValue()) { - // breadcrumbs might be disabled (also via a setting/config) and that is - // something we must check before proceeding. - return; - } - if (e.affectsConfiguration('breadcrumbs')) { - this._updateOutline(true); - return; - } - if (this._editor && this._editor.getModel()) { - const editorModel = this._editor.getModel() as ITextModel; - const languageName = editorModel.getLanguageIdentifier().language; - - // Checking for changes in the current language override config. - // We can't be more specific than this because the ConfigurationChangeEvent(e) only includes the first part of the root path - if (e.affectsConfiguration(`[${languageName}]`)) { - this._updateOutline(true); - } - } - })); - - - // update soon'ish as model content change - const updateSoon = new TimeoutTimer(); - this._disposables.add(updateSoon); - this._disposables.add(this._editor.onDidChangeModelContent(_ => { - const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); - updateSoon.cancelAndSet(() => this._updateOutline(true), timeout); - })); - this._updateOutline(); - - // stop when editor dies - this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); - } - - private _updateOutline(didChangeContent?: boolean): void { - + private _bindToEditor(editor: IEditorPane): void { + const newCts = new CancellationTokenSource(); + this._currentOutline.clear(); this._outlineDisposables.clear(); - if (!didChangeContent) { - this._updateOutlineElements([]); - } + this._outlineDisposables.add(toDisposable(() => newCts.dispose(true))); - const editor = this._editor!; - - const buffer = editor.getModel(); - if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) { - return; - } - - const source = new CancellationTokenSource(); - const versionIdThen = buffer.getVersionId(); - const timeout = new TimeoutTimer(); - - this._outlineDisposables.add({ - dispose: () => { - source.dispose(true); - timeout.dispose(); + this._outlineService.createOutline(editor, OutlineTarget.Breadcrumbs, newCts.token).then(outline => { + if (newCts.token.isCancellationRequested) { + // cancelled: dispose new outline and reset + outline?.dispose(); + outline = undefined; } - }); - - OutlineModel.create(buffer, source.token).then(model => { - if (source.token.isCancellationRequested) { - // cancelled -> do nothing - return; + this._currentOutline.value = outline; + this._onDidUpdate.fire(this); + if (outline) { + this._outlineDisposables.add(outline.onDidChange(() => this._onDidUpdate.fire(this))); } - if (TreeElement.empty(model)) { - // empty -> no outline elements - this._updateOutlineElements([]); - } else { - // copy the model - model = model.adopt(); - - this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - this._outlineDisposables.add(editor.onDidChangeCursorPosition(_ => { - timeout.cancelAndSet(() => { - if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && editor.getModel()) { - this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition())); - } - }, 150); - })); - } }).catch(err => { - this._updateOutlineElements([]); + this._onDidUpdate.fire(this); onUnexpectedError(err); }); } - - private _getOutlineElements(model: OutlineModel, position: IPosition | null): Array { - if (!model || !position) { - return []; - } - let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); - if (!item) { - return this._getOutlineElementsRoot(model); - } - let chain: Array = []; - while (item) { - chain.push(item); - let parent: any = item.parent; - if (parent instanceof OutlineModel) { - break; - } - if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) { - break; - } - item = parent; - } - let result: Array = []; - for (let i = chain.length - 1; i >= 0; i--) { - let element = chain[i]; - if (this._isFiltered(element)) { - break; - } - result.push(element); - } - if (result.length === 0) { - return this._getOutlineElementsRoot(model); - } - return result; - } - - private _getOutlineElementsRoot(model: OutlineModel): (OutlineModel | OutlineGroup | OutlineElement)[] { - for (const child of model.children.values()) { - if (!this._isFiltered(child)) { - return [model]; - } - } - return []; - } - - private _isFiltered(element: TreeElement): boolean { - if (element instanceof OutlineElement) { - const key = `breadcrumbs.${OutlineFilter.kindToConfigName[element.symbol.kind]}`; - let uri: URI | undefined; - if (this._editor && this._editor.getModel()) { - const model = this._editor.getModel() as ITextModel; - uri = model.uri; - } - return !this._textResourceConfigurationService.getValue(uri, key); - } - return false; - } - - private _updateOutlineElements(elements: Array): void { - if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) { - this._outlineElements = elements; - this._onDidUpdate.fire(this); - } - } - - private static _outlineElementEquals(a: OutlineModel | OutlineGroup | OutlineElement, b: OutlineModel | OutlineGroup | OutlineElement): boolean { - if (a === b) { - return true; - } else if (!a || !b) { - return false; - } else { - return a.id === b.id; - } - } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index b48d37e7b5..753e867a70 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -8,34 +8,30 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import * as glob from 'vs/base/common/glob'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; import { posix } from 'vs/base/common/path'; import { basename, dirname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; -import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; -import { IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchDataTree, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; -import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; +import { OutlineElement2, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; -import { OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItemComparator, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineDataSource, OutlineSortOrder, OutlineFilter, OutlineAccessibilityProvider } from 'vs/editor/contrib/documentSymbols/outlineTree'; import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; - -export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker { - return element instanceof FileElement - ? instantiationService.createInstance(BreadcrumbsFilePicker, parent) - : instantiationService.createInstance(BreadcrumbsOutlinePicker, parent); -} +import { IOutline, IOutlineComparator } from 'vs/workbench/services/outline/browser/outline'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; interface ILayoutInfo { maxHeight: number; @@ -62,17 +58,18 @@ export abstract class BreadcrumbsPicker { protected _fakeEvent = new UIEvent('fakeEvent'); protected _layoutInfo!: ILayoutInfo; - private readonly _onDidPickElement = new Emitter(); - readonly onDidPickElement: Event = this._onDidPickElement.event; + protected readonly _onWillPickElement = new Emitter(); + readonly onWillPickElement: Event = this._onWillPickElement.event; - private readonly _onDidFocusElement = new Emitter(); - readonly onDidFocusElement: Event = this._onDidFocusElement.event; + private readonly _previewDispoables = new MutableDisposable(); constructor( parent: HTMLElement, + protected resource: URI, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IThemeService protected readonly _themeService: IThemeService, @IConfigurationService protected readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons'; @@ -81,12 +78,13 @@ export abstract class BreadcrumbsPicker { dispose(): void { this._disposables.dispose(); - this._onDidPickElement.dispose(); - this._onDidFocusElement.dispose(); - this._tree.dispose(); + this._previewDispoables.dispose(); + this._onWillPickElement.dispose(); + this._domNode.remove(); + setTimeout(() => this._tree.dispose(), 0); // tree cannot be disposed while being opened... } - show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void { + async show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): Promise { const theme = this._themeService.getColorTheme(); const color = theme.getColor(breadcrumbsPickerBackground); @@ -103,33 +101,33 @@ export abstract class BreadcrumbsPicker { this._domNode.appendChild(this._treeContainer); this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 }; - this._tree = this._createTree(this._treeContainer); + this._tree = this._createTree(this._treeContainer, input); - this._disposables.add(this._tree.onDidChangeSelection(e => { - if (e.browserEvent !== this._fakeEvent) { - const target = this._getTargetFromEvent(e.elements[0]); - if (target) { - setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click - this._onDidPickElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); - }, 0); - } + this._disposables.add(this._tree.onDidOpen(async e => { + const { element, editorOptions, sideBySide } = e; + const didReveal = await this._revealElement(element, { ...editorOptions, preserveFocus: false }, sideBySide); + if (!didReveal) { + return; } + // send telemetry + interface OpenEvent { type: string } + interface OpenEventGDPR { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' } } + this._telemetryService.publicLog2('breadcrumbs/open', { type: element instanceof OutlineElement2 ? 'symbol' : 'file' }); })); this._disposables.add(this._tree.onDidChangeFocus(e => { - const target = this._getTargetFromEvent(e.elements[0]); - if (target) { - this._onDidFocusElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') }); - } + this._previewDispoables.value = this._previewElement(e.elements[0]); })); this._disposables.add(this._tree.onDidChangeContentHeight(() => { this._layout(); })); this._domNode.focus(); - - this._setInput(input).then(() => { + try { + await this._setInput(input); this._layout(); - }).catch(onUnexpectedError); + } catch (err) { + onUnexpectedError(err); + } } protected _layout(): void { @@ -146,16 +144,15 @@ export abstract class BreadcrumbsPicker { this._treeContainer.style.height = `${treeHeight}px`; this._treeContainer.style.width = `${this._layoutInfo.width}px`; this._tree.layout(treeHeight, this._layoutInfo.width); - } - get useAltAsMultipleSelectionModifier() { - return this._tree.useAltAsMultipleSelectionModifier; - } + restoreViewState(): void { } + + protected abstract _setInput(element: FileElement | OutlineElement2): Promise; + protected abstract _createTree(container: HTMLElement, input: any): Tree; + protected abstract _previewElement(element: any): IDisposable; + protected abstract _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise; - protected abstract _setInput(element: BreadcrumbElement): Promise; - protected abstract _createTree(container: HTMLElement): Tree; - protected abstract _getTargetFromEvent(element: any): any | undefined; } //#region - Files @@ -173,9 +170,9 @@ class FileIdentityProvider implements IIdentityProvider { - private readonly _parents = new WeakMap(); - constructor( @IFileService private readonly _fileService: IFileService, ) { } hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean { return URI.isUri(element) - || IWorkspace.isIWorkspace(element) - || IWorkspaceFolder.isIWorkspaceFolder(element) + || isWorkspace(element) + || isWorkspaceFolder(element) || element.isDirectory; } - getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { - - if (IWorkspace.isIWorkspace(element)) { - return Promise.resolve(element.folders).then(folders => { - for (let child of folders) { - this._parents.set(element, child); - } - return folders; - }); + async getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { + if (isWorkspace(element)) { + return element.folders; } let uri: URI; - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + if (isWorkspaceFolder(element)) { uri = element.uri; } else if (URI.isUri(element)) { uri = element; } else { uri = element.resource; } - return this._fileService.resolve(uri).then(stat => { - for (const child of stat.children || []) { - this._parents.set(stat, child); - } - return stat.children || []; - }); + const stat = await this._fileService.resolve(uri); + return stat.children ?? []; } } @@ -245,7 +230,7 @@ class FileRenderer implements ITreeRenderer { } filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean { - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + if (isWorkspaceFolder(element)) { // not a file return true; } @@ -345,7 +330,7 @@ class FileFilter implements ITreeFilter { export class FileSorter implements ITreeSorter { compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number { - if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) { + if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) { return a.index - b.index; } if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { @@ -363,12 +348,15 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { constructor( parent: HTMLElement, + protected resource: URI, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IConfigurationService configService: IConfigurationService, @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IEditorService private readonly _editorService: IEditorService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(parent, instantiationService, themeService, configService); + super(parent, resource, instantiationService, themeService, configService, telemetryService); } _createTree(container: HTMLElement) { @@ -406,7 +394,7 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { }); } - _setInput(element: BreadcrumbElement): Promise { + async _setInput(element: FileElement | OutlineElement2): Promise { const { uri, kind } = (element as FileElement); let input: IWorkspace | URI; if (kind === FileKind.ROOT_FOLDER) { @@ -416,115 +404,120 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { } const tree = this._tree as WorkbenchAsyncDataTree; - return tree.setInput(input).then(() => { - let focusElement: IWorkspaceFolder | IFileStat | undefined; - for (const { element } of tree.getNode().children) { - if (IWorkspaceFolder.isIWorkspaceFolder(element) && isEqual(element.uri, uri)) { - focusElement = element; - break; - } else if (isEqual((element as IFileStat).resource, uri)) { - focusElement = element as IFileStat; - break; - } + await tree.setInput(input); + let focusElement: IWorkspaceFolder | IFileStat | undefined; + for (const { element } of tree.getNode().children) { + if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) { + focusElement = element; + break; + } else if (isEqual((element as IFileStat).resource, uri)) { + focusElement = element as IFileStat; + break; } - if (focusElement) { - tree.reveal(focusElement, 0.5); - tree.setFocus([focusElement], this._fakeEvent); - } - tree.domFocus(); - }); + } + if (focusElement) { + tree.reveal(focusElement, 0.5); + tree.setFocus([focusElement], this._fakeEvent); + } + tree.domFocus(); } - protected _getTargetFromEvent(element: any): any | undefined { - if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) { - return new FileElement((element as IFileStat).resource, FileKind.FILE); + protected _previewElement(_element: any): IDisposable { + return Disposable.None; + } + + async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise { + let resource: URI | undefined; + if (isWorkspaceFolder(element)) { + resource = element.uri; + } else if (!element.isDirectory) { + resource = element.resource; } + if (resource) { + this._onWillPickElement.fire(); + await this._editorService.openEditor({ resource, options }, sideBySide ? SIDE_GROUP : undefined); + return true; + } + return false; } } //#endregion -//#region - Symbols +//#region - Outline + +class OutlineTreeSorter implements ITreeSorter { + + private _order: 'name' | 'type' | 'position'; + + constructor( + private comparator: IOutlineComparator, + uri: URI | undefined, + @ITextResourceConfigurationService configService: ITextResourceConfigurationService, + ) { + this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder'); + } + + compare(a: E, b: E): number { + if (this._order === 'name') { + return this.comparator.compareByName(a, b); + } else if (this._order === 'type') { + return this.comparator.compareByType(a, b); + } else { + return this.comparator.compareByPosition(a, b); + } + } +} export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { - protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>; - protected _outlineComparator: OutlineItemComparator; + protected _createTree(container: HTMLElement, input: OutlineElement2) { - constructor( - parent: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IModeService private readonly _modeService: IModeService, - ) { - super(parent, instantiationService, themeService, configurationService); - this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService); - this._outlineComparator = new OutlineItemComparator(); - } + const { config } = input.outline; - protected _createTree(container: HTMLElement) { - return >this._instantiationService.createInstance( + return , any, FuzzyScore>>this._instantiationService.createInstance( WorkbenchDataTree, 'BreadcrumbsOutlinePicker', container, - new OutlineVirtualDelegate(), - [new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)], - new OutlineDataSource(), + config.delegate, + config.renderers, + config.treeDataSource, { + ...config.options, + sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined), collapseByDefault: true, expandOnlyOnTwistieClick: true, multipleSelectionSupport: false, - sorter: this._outlineComparator, - identityProvider: new OutlineIdentityProvider(), - keyboardNavigationLabelProvider: new OutlineNavigationLabelProvider(), - accessibilityProvider: new OutlineAccessibilityProvider(localize('breadcrumbs', "Breadcrumbs")), - filter: this._instantiationService.createInstance(OutlineFilter, 'breadcrumbs') } ); } - dispose(): void { - this._symbolSortOrder.dispose(); - super.dispose(); - } + protected _setInput(input: OutlineElement2): Promise { - protected _setInput(input: BreadcrumbElement): Promise { - const element = input as TreeElement; - const model = OutlineModel.get(element)!; - const tree = this._tree as WorkbenchDataTree; + const viewState = input.outline.captureViewState(); + this.restoreViewState = () => { viewState.dispose(); }; - const overrideConfiguration = { - resource: model.uri, - overrideIdentifier: this._modeService.getModeIdByFilepathOrFirstLine(model.uri) - }; - this._outlineComparator.type = this._getOutlineItemCompareType(overrideConfiguration); + const tree = this._tree as WorkbenchDataTree, any, FuzzyScore>; - tree.setInput(model); - if (element !== model) { - tree.reveal(element, 0.5); - tree.setFocus([element], this._fakeEvent); + tree.setInput(input.outline); + if (input.element !== input.outline) { + tree.reveal(input.element, 0.5); + tree.setFocus([input.element], this._fakeEvent); } tree.domFocus(); return Promise.resolve(); } - protected _getTargetFromEvent(element: any): any | undefined { - if (element instanceof OutlineElement) { - return element; - } + protected _previewElement(element: any): IDisposable { + const outline: IOutline = this._tree.getInput(); + return outline.preview(element); } - private _getOutlineItemCompareType(overrideConfiguration?: IConfigurationOverrides): OutlineSortOrder { - switch (this._symbolSortOrder.getValue(overrideConfiguration)) { - case 'name': - return OutlineSortOrder.ByName; - case 'type': - return OutlineSortOrder.ByKind; - case 'position': - default: - return OutlineSortOrder.ByPosition; - } + async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise { + this._onWillPickElement.fire(); + const outline: IOutline = this._tree.getInput(); + await outline.reveal(element, options, sideBySide); + return true; } } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index fe47917a6d..d357c8c2c3 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -33,7 +33,7 @@ import { JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction, EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction, NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction, - QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction + QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction } from 'vs/workbench/browser/parts/editor/editorActions'; import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -42,7 +42,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/codeeditor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { toLocalResource } from 'vs/base/common/resources'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; @@ -351,6 +351,10 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupLeftAction, registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupRightAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.RightArrow) }), 'View: Move Editor Group Right', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupUpAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.UpArrow) }), 'View: Move Editor Group Up', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupDownAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.DownArrow) }), 'View: Move Editor Group Down', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupLeftAction), 'View: Duplicate Editor Group Left', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupRightAction), 'View: Duplicate Editor Group Right', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupUpAction), 'View: Duplicate Editor Group Up', CATEGORIES.View.value); +registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupDownAction), 'View: Duplicate Editor Group Down', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToPreviousGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow } }), 'View: Move Editor into Previous Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToNextGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow } }), 'View: Move Editor into Next Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToFirstGroupAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_1, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_1 } }), 'View: Move Editor into First Group', CATEGORIES.View.value); @@ -464,7 +468,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands. 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 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.KEEP_EDITORS_COMMAND_ID, title: nls.localize('toggleKeepEditors', "Keep Editors Open") }, when: ContextKeyExpr.has('config.workbench.editor.enablePreview'), group: '7_settings', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_KEEP_EDITORS_COMMAND_ID, title: nls.localize('toggleKeepEditors', "Keep Editors Open"), toggled: ContextKeyExpr.not('config.workbench.editor.enablePreview') }, group: '7_settings', order: 10 }); interface IEditorToolItem { id: string; title: string; icon?: { dark?: URI; light?: URI; } | ThemeIcon; } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 977ed42c13..923094a2fc 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -23,6 +23,7 @@ import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecent import { Codicon } from 'vs/base/common/codicons'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { openEditorWith, getAllAvailableEditors } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ExecuteCommandAction extends Action { @@ -497,7 +498,7 @@ export class RevertAndCloseEditorAction extends Action { await this.editorService.revert({ editor, groupId: group.id }, { soft: true }); } - group.closeEditor(editor); + return group.closeEditor(editor); } } } @@ -571,7 +572,7 @@ abstract class BaseCloseAllAction extends Action { // Otherwise ask for combined confirmation and make sure // to bring each dirty editor to the front so that the user // can review if the files should be changed or not. - await Promise.all(this.groupsToClose.map(async groupToClose => { + await Promise.all(this.groupsToClose.map(groupToClose => { for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: this.excludeSticky })) { if (editor.isDirty() && !editor.isSaving() /* ignore editors that are being saved */) { return groupToClose.openEditor(editor); @@ -746,12 +747,13 @@ export class CloseEditorInAllGroupsAction extends Action { } } -export class BaseMoveGroupAction extends Action { +class BaseMoveCopyGroupAction extends Action { constructor( id: string, label: string, private direction: GroupDirection, + private isMove: boolean, private editorGroupService: IEditorGroupsService ) { super(id, label); @@ -766,9 +768,18 @@ export class BaseMoveGroupAction extends Action { } if (sourceGroup) { - const targetGroup = this.findTargetGroup(sourceGroup); - if (targetGroup) { - this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); + let resultGroup: IEditorGroup | undefined = undefined; + if (this.isMove) { + const targetGroup = this.findTargetGroup(sourceGroup); + if (targetGroup) { + resultGroup = this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); + } + } else { + resultGroup = this.editorGroupService.copyGroup(sourceGroup, sourceGroup, this.direction); + } + + if (resultGroup) { + this.editorGroupService.activateGroup(resultGroup); } } } @@ -801,6 +812,18 @@ export class BaseMoveGroupAction extends Action { } } +class BaseMoveGroupAction extends BaseMoveCopyGroupAction { + + constructor( + id: string, + label: string, + direction: GroupDirection, + editorGroupService: IEditorGroupsService + ) { + super(id, label, direction, true, editorGroupService); + } +} + export class MoveGroupLeftAction extends BaseMoveGroupAction { static readonly ID = 'workbench.action.moveActiveEditorGroupLeft'; @@ -857,6 +880,74 @@ export class MoveGroupDownAction extends BaseMoveGroupAction { } } +class BaseDuplicateGroupAction extends BaseMoveCopyGroupAction { + + constructor( + id: string, + label: string, + direction: GroupDirection, + editorGroupService: IEditorGroupsService + ) { + super(id, label, direction, false, editorGroupService); + } +} + +export class DuplicateGroupLeftAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupLeft'; + static readonly LABEL = nls.localize('duplicateActiveGroupLeft', "Duplicate Editor Group Left"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.LEFT, editorGroupService); + } +} + +export class DuplicateGroupRightAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupRight'; + static readonly LABEL = nls.localize('duplicateActiveGroupRight', "Duplicate Editor Group Right"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.RIGHT, editorGroupService); + } +} + +export class DuplicateGroupUpAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupUp'; + static readonly LABEL = nls.localize('duplicateActiveGroupUp', "Duplicate Editor Group Up"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.UP, editorGroupService); + } +} + +export class DuplicateGroupDownAction extends BaseDuplicateGroupAction { + + static readonly ID = 'workbench.action.duplicateActiveEditorGroupDown'; + static readonly LABEL = nls.localize('duplicateActiveGroupDown', "Duplicate Editor Group Down"); + + constructor( + id: string, + label: string, + @IEditorGroupsService editorGroupService: IEditorGroupsService + ) { + super(id, label, GroupDirection.DOWN, editorGroupService); + } +} + export class MinimizeOtherGroupsAction extends Action { static readonly ID = 'workbench.action.minimizeOtherEditors'; @@ -1803,9 +1894,8 @@ export class ReopenResourcesAction extends Action { constructor( id: string, label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService, @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(id, label); } @@ -1823,7 +1913,7 @@ export class ReopenResourcesAction extends Action { const options = activeEditorPane.options; const group = activeEditorPane.group; - await openEditorWith(activeInput, undefined, options, group, this.editorService, this.configurationService, this.quickInputService); + await this.instantiationService.invokeFunction(openEditorWith, activeInput, undefined, options, group); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 544688f632..4783ce04d8 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -23,7 +23,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; @@ -40,7 +39,7 @@ export const CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeOt export const MOVE_ACTIVE_EDITOR_COMMAND_ID = 'moveActiveEditor'; export const LAYOUT_EDITOR_GROUPS_COMMAND_ID = 'layoutEditorGroups'; export const KEEP_EDITOR_COMMAND_ID = 'workbench.action.keepEditor'; -export const KEEP_EDITORS_COMMAND_ID = 'workbench.action.keepEditors'; +export const TOGGLE_KEEP_EDITORS_COMMAND_ID = 'workbench.action.toggleKeepEditors'; export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup'; export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor'; @@ -484,7 +483,6 @@ function registerOpenEditorAPICommands(): void { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const [columnArg, optionsArg] = columnAndOptions ?? []; let group: IEditorGroup | undefined = undefined; @@ -504,7 +502,7 @@ function registerOpenEditorAPICommands(): void { const textOptions: ITextEditorOptions = optionsArg ? { ...optionsArg, override: false } : { override: false }; const input = editorService.createEditorInput({ resource: URI.revive(resource) }); - return openEditorWith(input, id, textOptions, group, editorService, configurationService, quickInputService); + return openEditorWith(accessor, input, id, textOptions, group); }); } @@ -899,23 +897,13 @@ function registerOtherEditorCommands(): void { }); CommandsRegistry.registerCommand({ - id: KEEP_EDITORS_COMMAND_ID, + id: TOGGLE_KEEP_EDITORS_COMMAND_ID, handler: accessor => { const configurationService = accessor.get(IConfigurationService); - const notificationService = accessor.get(INotificationService); - const openerService = accessor.get(IOpenerService); - // Update setting - configurationService.updateValue('workbench.editor.enablePreview', false); - - // Inform user - notificationService.prompt( - Severity.Info, - nls.localize('disablePreview', "Preview editors have been disabled in settings."), - [{ - label: nls.localize('learnMode', "Learn More"), run: () => openerService.open('https://go.microsoft.com/fwlink/?linkid=2147473') - }] - ); + const currentSetting = configurationService.getValue('workbench.editor.enablePreview'); + const newSetting = currentSetting === true ? false : true; + configurationService.updateValue('workbench.editor.enablePreview', newSetting); } }); diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 36f76313bd..d89e2ccc35 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -287,7 +287,8 @@ class DropOverlay extends Themable { // Open in target group const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state + sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state + override: false, // Use `draggedEditor.editor` as is. If it is already a custom editor, it will stay so. })); const copyEditor = this.isCopyOperation(event, draggedEditor); targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 9d922d9d76..426adce5e9 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -27,7 +27,7 @@ import { dispose, MutableDisposable } from 'vs/base/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { RunOnceWorker } from 'vs/base/common/async'; +import { Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions, IEditorOpeningEvent, EditorServiceImpl, IEditorGroupTitleDimensions } from 'vs/workbench/browser/parts/editor/editor'; @@ -322,8 +322,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private createContainerContextMenu(): void { const menu = this._register(this.menuService.createMenu(MenuId.EmptyEditorGroupContext, this.contextKeyService)); - this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, event => this.onShowContainerContextMenu(menu, event))); - this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, event => this.onShowContainerContextMenu(menu))); + this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => this.onShowContainerContextMenu(menu, e))); + this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, () => this.onShowContainerContextMenu(menu))); } private onShowContainerContextMenu(menu: IMenu, e?: MouseEvent): void { @@ -1111,7 +1111,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Open the other ones inactive const startingIndex = this.getIndexOfEditor(editor) + 1; - await Promise.all(editors.map(async ({ editor, options }, index) => { + await Promises.settled(editors.map(async ({ editor, options }, index) => { const adjustedEditorOptions = options || new EditorOptions(); adjustedEditorOptions.inactive = true; adjustedEditorOptions.pinned = true; @@ -1348,10 +1348,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.mapEditorToPendingConfirmation.set(editor, handleDirtyClosingPromise); } - const veto = await handleDirtyClosingPromise; - - // Make sure to remove from our map of cached pending confirmations - this.mapEditorToPendingConfirmation.delete(editor); + let veto: boolean; + try { + veto = await handleDirtyClosingPromise; + } finally { + this.mapEditorToPendingConfirmation.delete(editor); + } // Return for the first veto we got if (veto) { @@ -1705,13 +1707,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { layout(width: number, height: number): void { this.dimension = new Dimension(width, height); - // Ensure editor container gets height as CSS depending on the preferred height of the title control - const titleHeight = this.titleDimensions.height; - const editorHeight = Math.max(0, height - titleHeight); - this.editorContainer.style.height = `${editorHeight}px`; + // Layout the title area first to receive the size it occupies + const titleAreaSize = this.titleAreaControl.layout({ + container: this.dimension, + available: new Dimension(width, height - this.editorControl.minimumHeight) + }); - // Forward to controls - this.titleAreaControl.layout(new Dimension(width, titleHeight)); + // Pass the container width and remaining height to the editor layout + const editorHeight = Math.max(0, height - titleAreaSize.height); + this.editorContainer.style.height = `${editorHeight}px`; this.editorControl.layout(new Dimension(width, editorHeight)); } @@ -1770,7 +1774,7 @@ export interface EditorReplacement { readonly options?: EditorOptions; } -registerThemingParticipant((theme, collector, environment) => { +registerThemingParticipant((theme, collector) => { // Letterpress const letterpress = `./media/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`; diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 2be8346e30..f382d100fd 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/workbench/browser/parts/editor/editor.contribution'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; import { Dimension, isAncestor, $, EventHelper, addDisposableGenericMouseDownListner } from 'vs/base/browser/dom'; @@ -32,6 +31,7 @@ import { MementoObject } from 'vs/workbench/common/memento'; import { assertIsDefined } from 'vs/base/common/types'; import { IBoundarySashes } from 'vs/base/browser/ui/grid/gridview'; import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { Promises } from 'vs/base/common/async'; interface IEditorPartUIState { serializedGrid: ISerializedGrid; @@ -944,7 +944,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } // Signal restored - Promise.all(this.groups.map(group => group.whenRestored)).finally(() => { + Promises.settled(this.groups.map(group => group.whenRestored)).finally(() => { if (this.whenRestoredResolve) { this.whenRestoredResolve(); } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index d33d8bb50d..84d0f2b75d 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editorstatus'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; import { format, compare, splitLines } from 'vs/base/common/strings'; import { extname, basename, isEqual } from 'vs/base/common/resources'; @@ -286,14 +286,15 @@ class State { } } -const nlsSingleSelectionRange = nls.localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); -const nlsSingleSelection = nls.localize('singleSelection', "Ln {0}, Col {1}"); -const nlsMultiSelectionRange = nls.localize('multiSelectionRange', "{0} selections ({1} characters selected)"); -const nlsMultiSelection = nls.localize('multiSelection', "{0} selections"); -const nlsEOLLF = nls.localize('endOfLineLineFeed', "LF"); -const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF"); +const nlsSingleSelectionRange = localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)"); +const nlsSingleSelection = localize('singleSelection', "Ln {0}, Col {1}"); +const nlsMultiSelectionRange = localize('multiSelectionRange', "{0} selections ({1} characters selected)"); +const nlsMultiSelection = localize('multiSelection', "{0} selections"); +const nlsEOLLF = localize('endOfLineLineFeed', "LF"); +const nlsEOLCRLF = localize('endOfLineCarriageReturnLineFeed', "CRLF"); export class EditorStatus extends Disposable implements IWorkbenchContribution { + private readonly tabFocusModeElement = this._register(new MutableDisposable()); private readonly columnSelectionModeElement = this._register(new MutableDisposable()); private readonly screenRedearModeElement = this._register(new MutableDisposable()); @@ -345,14 +346,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.screenReaderNotification) { this.screenReaderNotification = this.notificationService.prompt( Severity.Info, - nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate Azure Data Studio? (word wrap is disabled when using a screen reader)"), // {{SQL CARBON EDIT}} change vscode to ads + localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"), [{ - label: nls.localize('screenReaderDetectedExplanation.answerYes', "Yes"), + label: localize('screenReaderDetectedExplanation.answerYes', "Yes"), run: () => { this.configurationService.updateValue('editor.accessibilitySupport', 'on'); } }, { - label: nls.localize('screenReaderDetectedExplanation.answerNo', "No"), + label: localize('screenReaderDetectedExplanation.answerNo', "No"), run: () => { this.configurationService.updateValue('editor.accessibilitySupport', 'off'); } @@ -367,11 +368,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private async showIndentationPicker(): Promise { const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); } if (this.editorService.activeEditor?.isReadonly()) { - return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } const picks: QuickPickInput[] = [ @@ -393,25 +394,25 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { }; }); - picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") }); - picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") }); + picks.splice(3, 0, { type: 'separator', label: localize('indentConvert', "convert file") }); + picks.unshift({ type: 'separator', label: localize('indentView', "change view") }); - const action = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + const action = await this.quickInputService.pick(picks, { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); return action?.run(); } private updateTabFocusModeElement(visible: boolean): void { if (visible) { if (!this.tabFocusModeElement.value) { - const text = nls.localize('tabFocusModeEnabled', "Tab Moves Focus"); + const text = localize('tabFocusModeEnabled', "Tab Moves Focus"); this.tabFocusModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('disableTabMode', "Disable Accessibility Mode"), + tooltip: localize('disableTabMode', "Disable Accessibility Mode"), command: 'editor.action.toggleTabFocusMode', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.tabFocusMode', nls.localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); + }, 'status.editor.tabFocusMode', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); } } else { this.tabFocusModeElement.clear(); @@ -421,15 +422,15 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private updateColumnSelectionModeElement(visible: boolean): void { if (visible) { if (!this.columnSelectionModeElement.value) { - const text = nls.localize('columnSelectionModeEnabled', "Column Selection"); + const text = localize('columnSelectionModeEnabled', "Column Selection"); this.columnSelectionModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('disableColumnSelectionMode', "Disable Column Selection Mode"), + tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"), command: 'editor.action.toggleColumnSelection', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.columnSelectionMode', nls.localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + }, 'status.editor.columnSelectionMode', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); } } else { this.columnSelectionModeElement.clear(); @@ -439,14 +440,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private updateScreenReaderModeElement(visible: boolean): void { if (visible) { if (!this.screenRedearModeElement.value) { - const text = nls.localize('screenReaderDetected', "Screen Reader Optimized"); + const text = localize('screenReaderDetected', "Screen Reader Optimized"); this.screenRedearModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.screenReaderMode', nls.localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); + }, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); } } else { this.screenRedearModeElement.clear(); @@ -462,11 +463,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('gotoLine', "Go to Line/Column"), + tooltip: localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; - this.updateElement(this.selectionElement, props, 'status.editor.selection', nls.localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); + this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); } private updateIndentationElement(text: string | undefined): void { @@ -478,11 +479,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectIndentation', "Select Indentation"), + tooltip: localize('selectIndentation', "Select Indentation"), command: 'changeEditorIndentation' }; - this.updateElement(this.indentationElement, props, 'status.editor.indentation', nls.localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); + this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); } private updateEncodingElement(text: string | undefined): void { @@ -494,11 +495,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectEncoding', "Select Encoding"), + tooltip: localize('selectEncoding', "Select Encoding"), command: 'workbench.action.editor.changeEncoding' }; - this.updateElement(this.encodingElement, props, 'status.editor.encoding', nls.localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); + this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); } private updateEOLElement(text: string | undefined): void { @@ -510,11 +511,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectEOL', "Select End of Line Sequence"), + tooltip: localize('selectEOL', "Select End of Line Sequence"), command: 'workbench.action.editor.changeEOL' }; - this.updateElement(this.eolElement, props, 'status.editor.eol', nls.localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); + this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); } private updateModeElement(text: string | undefined): void { @@ -526,11 +527,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('selectLanguageMode', "Select Language Mode"), + tooltip: localize('selectLanguageMode', "Select Language Mode"), command: 'workbench.action.editor.changeLanguageMode' }; - this.updateElement(this.modeElement, props, 'status.editor.mode', nls.localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); + this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); } private updateMetadataElement(text: string | undefined): void { @@ -542,10 +543,10 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const props: IStatusbarEntry = { text, ariaLabel: text, - tooltip: nls.localize('fileInfo', "File Information") + tooltip: localize('fileInfo', "File Information") }; - this.updateElement(this.metadataElement, props, 'status.editor.info', nls.localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); + this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); } private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { @@ -733,8 +734,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const modelOpts = model.getOptions(); update.indentation = ( modelOpts.insertSpaces - ? nls.localize('spacesSize', "Spaces: {0}", modelOpts.indentSize) - : nls.localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize) + ? localize('spacesSize', "Spaces: {0}", modelOpts.indentSize) + : localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize) ); } } @@ -755,7 +756,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private onColumnSelectionModeChange(editorWidget: ICodeEditor | undefined): void { const info: StateDelta = { type: 'columnSelectionMode', columnSelectionMode: false }; - if (editorWidget && editorWidget.getOption(EditorOption.columnSelection)) { + if (editorWidget?.getOption(EditorOption.columnSelection)) { info.columnSelectionMode = true; } @@ -769,7 +770,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (editorWidget) { const screenReaderDetected = this.accessibilityService.isScreenReaderOptimized(); if (screenReaderDetected) { - const screenReaderConfiguration = this.configurationService.getValue('editor').accessibilitySupport; + const screenReaderConfiguration = this.configurationService.getValue('editor')?.accessibilitySupport; if (screenReaderConfiguration === 'auto') { if (!this.promptedScreenReader) { this.promptedScreenReader = true; @@ -924,7 +925,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { const line = splitLines(this.currentMarker.message)[0]; const text = `${this.getType(this.currentMarker)} ${line}`; if (!this.statusBarEntryAccessor.value) { - this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', nls.localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); } this.statusBarEntryAccessor.value.update({ text, ariaLabel: text }); } else { @@ -937,9 +938,11 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!currentMarker) { return true; } + if (!previousMarker) { return true; } + return IMarkerData.makeKey(previousMarker) !== IMarkerData.makeKey(currentMarker); } @@ -949,6 +952,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { case MarkerSeverity.Warning: return '$(warning)'; case MarkerSeverity.Info: return '$(info)'; } + return ''; } @@ -956,17 +960,21 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.configurationService.getValue('problems.showCurrentInStatus')) { return null; } + if (!this.editor) { return null; } + const model = this.editor.getModel(); if (!model) { return null; } + const position = this.editor.getPosition(); if (!position) { return null; } + return this.markers.find(marker => Range.containsPosition(marker, position)) || null; } @@ -974,13 +982,16 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.editor) { return; } + const model = this.editor.getModel(); if (!model) { return; } + if (model && !changedResources.some(r => isEqual(model.uri, r))) { return; } + this.updateMarkers(); } @@ -988,10 +999,12 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { if (!this.editor) { return; } + const model = this.editor.getModel(); if (!model) { return; } + if (model) { this.markers = this.markerService.read({ resource: model.uri, @@ -1001,6 +1014,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { } else { this.markers = []; } + this.updateStatus(); } } @@ -1010,9 +1024,11 @@ function compareMarker(a: IMarker, b: IMarker): number { if (res === 0) { res = MarkerSeverity.compare(a.severity, b.severity); } + if (res === 0) { res = Range.compareRangesUsingStarts(a, b); } + return res; } @@ -1025,7 +1041,7 @@ export class ShowLanguageExtensionsAction extends Action { @ICommandService private readonly commandService: ICommandService, @IExtensionGalleryService galleryService: IExtensionGalleryService ) { - super(ShowLanguageExtensionsAction.ID, nls.localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); + super(ShowLanguageExtensionsAction.ID, localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); this.enabled = galleryService.isEnabled(); } @@ -1038,7 +1054,7 @@ export class ShowLanguageExtensionsAction extends Action { export class ChangeModeAction extends Action { static readonly ID = 'workbench.action.editor.changeLanguageMode'; - static readonly LABEL = nls.localize('changeMode', "Change Language Mode"); + static readonly LABEL = localize('changeMode', "Change Language Mode"); constructor( actionId: string, @@ -1057,14 +1073,14 @@ export class ChangeModeAction extends Action { async run(): Promise { const activeEditorPane = this.editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; - if (activeEditorPane?.isNotebookEditor) { + if (activeEditorPane?.isNotebookEditor) { // TODO@rebornix TODO@jrieken debt: https://github.com/microsoft/vscode/issues/114554 // it's inside notebook editor return this.commandService.executeCommand('notebook.cell.changeLanguage'); } const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } @@ -1088,22 +1104,24 @@ export class ChangeModeAction extends Action { const languages = this.modeService.getRegisteredLanguageNames(); const picks: QuickPickInput[] = languages.sort().map((lang, index) => { const modeId = this.modeService.getModeIdForLanguageName(lang.toLowerCase()) || 'unknown'; + const extensions = this.modeService.getExtensions(lang).join(' '); let description: string; if (currentLanguageId === lang) { - description = nls.localize('languageDescription', "({0}) - Configured Language", modeId); + description = localize('languageDescription', "({0}) - Configured Language", modeId); } else { - description = nls.localize('languageDescriptionConfigured', "({0})", modeId); + description = localize('languageDescriptionConfigured', "({0})", modeId); } return { label: lang, + meta: extensions, iconClasses: getIconClassesForModeId(modeId), description }; }); if (hasLanguageSupport) { - picks.unshift({ type: 'separator', label: nls.localize('languagesPicks', "languages (identifier)") }); + picks.unshift({ type: 'separator', label: localize('languagesPicks', "languages (identifier)") }); } // Offer action to configure via settings @@ -1118,22 +1136,22 @@ export class ChangeModeAction extends Action { picks.unshift(galleryAction); } - configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; + configureModeSettings = { label: localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; picks.unshift(configureModeSettings); - configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; + configureModeAssociations = { label: localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; picks.unshift(configureModeAssociations); } // Offer to "Auto Detect" const autoDetectMode: IQuickPickItem = { - label: nls.localize('autoDetect', "Auto Detect") + label: localize('autoDetect', "Auto Detect") }; if (hasLanguageSupport) { picks.unshift(autoDetectMode); } - const pick = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); + const pick = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true }); if (!pick) { return; } @@ -1181,6 +1199,8 @@ export class ChangeModeAction extends Action { return this.instantiationService.invokeFunction(setMode, modeSupport, activeEditor, languageSelection.languageIdentifier.language); // {{SQL CARBON EDIT}} @anthonydresser use custom setMode } } + + activeTextEditorControl.focus(); } } @@ -1197,12 +1217,12 @@ export class ChangeModeAction extends Action { id, label: lang, iconClasses: getIconClassesForModeId(id), - description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : undefined + description: (id === currentAssociation) ? localize('currentAssociation', "Current Association") : undefined }; }); setTimeout(async () => { - const language = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); + const language = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); if (language) { const fileAssociationsConfig = this.configurationService.inspect<{}>(FILES_ASSOCIATIONS_CONFIG); @@ -1236,7 +1256,7 @@ export interface IChangeEOLEntry extends IQuickPickItem { export class ChangeEOLAction extends Action { static readonly ID = 'workbench.action.editor.changeEOL'; - static readonly LABEL = nls.localize('changeEndOfLine', "Change End of Line Sequence"); + static readonly LABEL = localize('changeEndOfLine', "Change End of Line Sequence"); constructor( actionId: string, @@ -1250,12 +1270,12 @@ export class ChangeEOLAction extends Action { async run(): Promise { const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); if (!activeTextEditorControl) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } if (this.editorService.activeEditor?.isReadonly()) { - await this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + await this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); return; } @@ -1268,7 +1288,7 @@ export class ChangeEOLAction extends Action { const selectedIndex = (textModel?.getEOL() === '\n') ? 0 : 1; - const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); + const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); if (eol) { const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl); if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.isReadonly()) { @@ -1278,13 +1298,15 @@ export class ChangeEOLAction extends Action { textModel.pushStackElement(); } } + + activeTextEditorControl.focus(); } } export class ChangeEncodingAction extends Action { static readonly ID = 'workbench.action.editor.changeEncoding'; - static readonly LABEL = nls.localize('changeEncoding', "Change File Encoding"); + static readonly LABEL = localize('changeEncoding', "Change File Encoding"); constructor( actionId: string, @@ -1299,25 +1321,26 @@ export class ChangeEncodingAction extends Action { } async run(): Promise { - if (!getCodeEditor(this.editorService.activeTextEditorControl)) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl); + if (!activeTextEditorControl) { + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } const activeEditorPane = this.editorService.activeEditorPane; if (!activeEditorPane) { - await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); return; } const encodingSupport: IEncodingSupport | null = toEditorWithEncodingSupport(activeEditorPane.input); if (!encodingSupport) { - await this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); + await this.quickInputService.pick([{ label: localize('noFileEditor', "No file active at this time") }]); return; } - const saveWithEncodingPick: IQuickPickItem = { label: nls.localize('saveWithEncoding', "Save with Encoding") }; - const reopenWithEncodingPick: IQuickPickItem = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding") }; + const saveWithEncodingPick: IQuickPickItem = { label: localize('saveWithEncoding', "Save with Encoding") }; + const reopenWithEncodingPick: IQuickPickItem = { label: localize('reopenWithEncoding', "Reopen with Encoding") }; if (!Language.isDefaultVariant()) { const saveWithEncodingAlias = 'Save with Encoding'; @@ -1337,7 +1360,7 @@ export class ChangeEncodingAction extends Action { } else if (activeEditorPane.input.isReadonly()) { action = reopenWithEncodingPick; } else { - action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); } if (!action) { @@ -1397,11 +1420,11 @@ export class ChangeEncodingAction extends Action { // If we have a guessed encoding, show it first unless it matches the configured encoding if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) { picks.unshift({ type: 'separator' }); - picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: nls.localize('guessedEncoding', "Guessed from content") }); + picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: localize('guessedEncoding', "Guessed from content") }); } const encoding = await this.quickInputService.pick(picks, { - placeHolder: isReopenWithEncoding ? nls.localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : nls.localize('pickEncodingForSave', "Select File Encoding to Save with"), + placeHolder: isReopenWithEncoding ? localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : localize('pickEncodingForSave', "Select File Encoding to Save with"), activeItem: items[typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : -1] }); @@ -1417,5 +1440,7 @@ export class ChangeEncodingAction extends Action { if (typeof encoding.id !== 'undefined' && activeEncodingSupport && activeEncodingSupport.getEncoding() !== encoding.id) { activeEncodingSupport.setEncoding(encoding.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode); // Set new encoding } + + activeTextEditorControl.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 457578a682..5a7b662eb8 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -14,7 +14,7 @@ flex: auto; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label { line-height: 35px; overflow: hidden; text-overflow: ellipsis; @@ -22,16 +22,16 @@ padding-left: 20px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { - flex: none; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { - height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ +.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label > .monaco-icon-label-container { + flex: none; /* helps to show decorations right next to the label and not at the end */ } /* Breadcrumbs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label { + flex: none; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control { flex: 1 50%; overflow: hidden; @@ -62,13 +62,11 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder + .monaco-breadcrumb-item::before, .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.relative-path .monaco-breadcrumb-item:nth-child(2)::before, .monaco-workbench.windows .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:nth-child(2)::before { - /* workspace folder, item following workspace folder, or relative path -> hide first seperator */ - display: none; + display: none; /* workspace folder, item following workspace folder, or relative path -> hide first seperator */ } .monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::after { - /* use dot separator for workspace folder */ - content: '\00a0•\00a0'; + content: '\00a0•\00a0'; /* use dot separator for workspace folder */ padding: 0; } @@ -80,13 +78,18 @@ padding: 0 1px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { - display: none; /* hides chevrons when no tabs visible and when last items */ +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child { + display: none; /* hides chevrons when no tabs visible */ } -/* Title Actions */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-icon-label::before { + height: 18px; + padding-right: 2px; +} + +/* Editor Actions Toolbar (via title actions) */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title > .title-actions { display: flex; flex: initial; opacity: 0.5; @@ -94,6 +97,6 @@ height: 35px; } -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .title-actions { +.monaco-workbench .part.editor > .content .editor-group-container.active > .title > .title-actions { opacity: 1; } diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index cf00ba9e30..30a0b70ae5 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -3,17 +3,45 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* + ################################### z-index explainer ################################### + + Tabs have various levels of z-index depending on state, typically: + - scrollbar should be above all + - sticky (compact, shrink) tabs need to be above non-sticky tabs for scroll under effect + including non-sticky tabs-top borders, otherwise these borders would not scroll under + (https://github.com/microsoft/vscode/issues/111641) + - bottom-border needs to be above tabs bottom border to win but also support sticky tabs + (https://github.com/microsoft/vscode/issues/99084) <- this currently cannot be done and + is still broken. putting sticky-tabs above tabs bottom border would not render this + border at all for sticky tabs. + + On top of that there is 2 borders with a z-index for a general border below tabs + - tabs bottom border + - editor title bottom border (when breadcrumbs are disabled, this border will appear + same as tabs bottom border) + + The following tabls shows the current stacking order: + + [z-index] [kind] + 7 scrollbar + 6 active-tab border-bottom + 5 tabs, title border bottom + 4 sticky-tab + 2 active/dirty-tab border top + 0 tab + + ########################################################################################## +*/ + /* Title Container */ -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { display: flex; + position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom { - position: relative; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.tabs-border-bottom::after { content: ''; position: absolute; bottom: 0; @@ -25,12 +53,12 @@ height: 1px; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element { +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element { flex: 1; } -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { - z-index: 3; /* on top of tabs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { + z-index: 7; cursor: default; } @@ -46,6 +74,13 @@ overflow: scroll !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container { + + /* Enable wrapping via flex layout and dynamic height */ + height: auto; + flex-wrap: wrap; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar { display: none; /* Chrome + Safari: hide scrollbar */ } @@ -62,6 +97,10 @@ padding-left: 10px; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { + margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-right, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-off:not(.sticky-compact) { padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab actions is not left (unless sticky-compact) */ @@ -74,6 +113,10 @@ flex-shrink: 0; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab.sizing-fit { + flex-grow: 1; /* grow the tabs to fill each row for a more homogeneous look when tabs wrap */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { min-width: 80px; flex-basis: 0; /* all tabs are even */ @@ -89,7 +132,7 @@ /** Sticky compact/shrink tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ position: sticky; - z-index: 1; + z-index: 4; /** Sticky compact/shrink tabs are even and never grow */ flex-basis: 0; @@ -118,9 +161,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink { - - /** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */ - position: static; + position: static; /** disable sticky positions for sticky compact/shrink tabs if the available space is too little */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left .action-label { @@ -132,7 +173,7 @@ content: ''; display: flex; flex: 0; - width: 5px; /* Reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ + width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left { @@ -167,24 +208,26 @@ display: block; position: absolute; left: 0; - z-index: 6; /* over possible title border */ pointer-events: none; width: 100%; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 1px; background-color: var(--tab-border-top-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { + z-index: 6; bottom: 0; height: 1px; background-color: var(--tab-border-bottom-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 2px; background-color: var(--tab-dirty-border-top-color); @@ -203,13 +246,16 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after { - content: ''; + content: ''; /* enables a linear gradient to overlay the end of the label when tabs overflow */ position: absolute; right: 0; - height: 100%; width: 5px; opacity: 1; padding: 0; + /* the rules below ensure that the gradient does not impact top/bottom borders (https://github.com/microsoft/vscode/issues/115129) */ + top: 1px; + bottom: 1px; + height: calc(100% - 2px); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after { @@ -243,7 +289,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions { flex: 0; - overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room... */ + overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions, @@ -301,7 +347,7 @@ margin-right: 0.5em; } -/* No Tab Actions */ +/* Tab Actions: Off */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-off { padding-right: 10px; /* give a little bit more room if tab actions is off */ @@ -324,15 +370,6 @@ pointer-events: none; /* don't allow tab actions to be clicked when running without tab actions */ } -/* Editor Actions */ - -.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { - cursor: default; - flex: initial; - padding: 0 8px 0 4px; - height: 35px; -} - /* Breadcrumbs */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { @@ -350,11 +387,17 @@ height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .outline-element-icon { + padding-right: 3px; + height: 22px; /* tweak the icon size of the editor labels when icons are enabled */ + line-height: 22px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item { max-width: 80%; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { width: 16px; height: 22px; display: flex; @@ -362,6 +405,27 @@ justify-content: center; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child { padding-right: 8px; } + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child { + display: none; /* hides chevrons when last item */ +} + +/* Editor Actions Toolbar */ + +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions { + cursor: default; + flex: initial; + padding: 0 8px 0 4px; + height: 35px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-actions { + + /* When tabs are wrapped, position the editor actions at the end of the very last row */ + position: absolute; + bottom: 0; + right: 0; +} diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index be977a2a2f..4bbdac746e 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -26,8 +26,13 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::after { - padding-right: 0; +.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before { + height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .monaco-icon-label::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .monaco-icon-label::after { + padding-right: 0; /* by default the icon label has a padding right and this isn't wanted when not showing tabs and not showing breadcrumbs */ } /* Title Actions */ @@ -63,13 +68,12 @@ opacity: 0.4; } -/* Drag Cursor */ +/* Drag and Drop */ + .monaco-workbench .part.editor > .content .editor-group-container > .title { cursor: grab; } -/* Drag and Drop Feedback */ - .monaco-editor-group-drag-image { display: inline-block; padding: 1px 7px; diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 00e9e0593f..976452b95b 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/notabstitlecontrol'; import { EditorResourceAccessor, Verbosity, IEditorInput, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; -import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/titleControl'; +import { TitleControl, IToolbarActions, ITitleControlDimensions } from 'vs/workbench/browser/parts/editor/titleControl'; import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -51,7 +51,7 @@ export class NoTabsTitleControl extends TitleControl { // Breadcrumbs this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent }); titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl)); - this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // import to remove because the container is a shared dom node + this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // important to remove because the container is a shared dom node // Right Actions Container const actionsContainer = document.createElement('div'); @@ -68,16 +68,16 @@ export class NoTabsTitleControl extends TitleControl { this.enableGroupDragging(titleContainer); // Pin on double click - this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e))); + this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, e => this.onTitleDoubleClick(e))); // Detect mouse click - this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, (e: MouseEvent) => this.onTitleAuxClick(e))); + this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, e => this.onTitleAuxClick(e))); // Detect touch this._register(addDisposableListener(titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleTap(e))); // Context Menu - this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, (e: Event) => { + this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, e => { if (this.group.activeEditor) { this.onContextMenu(this.group.activeEditor, e, titleContainer); } @@ -189,7 +189,7 @@ export class NoTabsTitleControl extends TitleControl { } updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { - if (oldOptions.labelFormat !== newOptions.labelFormat || !equals(oldOptions.tabDecorations, newOptions.tabDecorations)) { + if (oldOptions.labelFormat !== newOptions.labelFormat || !equals(oldOptions.decorations, newOptions.decorations)) { this.redraw(); } } @@ -295,8 +295,8 @@ export class NoTabsTitleControl extends TitleControl { italic: !isEditorPinned, extraClasses: ['no-tabs', 'title-label'], fileDecorations: { - colors: Boolean(options.tabDecorations?.colors), - badges: Boolean(options.tabDecorations?.badges) + colors: Boolean(options.decorations?.colors), + badges: Boolean(options.decorations?.badges) }, } ); @@ -339,9 +339,11 @@ export class NoTabsTitleControl extends TitleControl { }; } - layout(dimension: Dimension): void { + layout(dimensions: ITitleControlDimensions): Dimension { if (this.breadcrumbsControl) { this.breadcrumbsControl.layout(undefined); } + + return new Dimension(dimensions.container.width, this.getDimensions().height); } } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index eddc9dce14..27a98a8a6c 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -76,7 +76,7 @@ export class SideBySideEditor extends EditorPane { this.secondaryEditorContainer = DOM.$('.secondary-editor-container'); this.splitview.addView({ element: this.secondaryEditorContainer, - layout: size => this.secondaryEditorPane && this.secondaryEditorPane.layout(new DOM.Dimension(size, this.dimension.height)), + layout: size => this.secondaryEditorPane?.layout(new DOM.Dimension(size, this.dimension.height)), minimumSize: 220, maximumSize: Number.POSITIVE_INFINITY, onDidChange: Event.None @@ -85,7 +85,7 @@ export class SideBySideEditor extends EditorPane { this.primaryEditorContainer = DOM.$('.primary-editor-container'); this.splitview.addView({ element: this.primaryEditorContainer, - layout: size => this.primaryEditorPane && this.primaryEditorPane.layout(new DOM.Dimension(size, this.dimension.height)), + layout: size => this.primaryEditorPane?.layout(new DOM.Dimension(size, this.dimension.height)), minimumSize: 220, maximumSize: Number.POSITIVE_INFINITY, onDidChange: Event.None diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index ca48e4c8ee..667c6854c2 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -18,19 +18,18 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IDisposable, dispose, DisposableStore, combinedDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; @@ -78,6 +77,9 @@ export class TabsTitleControl extends TitleControl { private static readonly TAB_HEIGHT = 35; + private static readonly MOUSE_WHEEL_EVENT_THRESHOLD = 150; + private static readonly MOUSE_WHEEL_DISTANCE_THRESHOLD = 1.5; + private titleContainer: HTMLElement | undefined; private tabsAndActionsContainer: HTMLElement | undefined; private tabsContainer: HTMLElement | undefined; @@ -92,12 +94,18 @@ export class TabsTitleControl extends TitleControl { private tabActionBars: ActionBar[] = []; private tabDisposables: IDisposable[] = []; - private dimension: Dimension | undefined; + private dimensions: ITitleControlDimensions & { used?: Dimension } = { + container: Dimension.None, + available: Dimension.None + }; + private readonly layoutScheduled = this._register(new MutableDisposable()); private blockRevealActiveTab: boolean | undefined; private path: IPath = isWindows ? win32 : posix; + private lastMouseWheelEventTime = 0; + constructor( parent: HTMLElement, accessor: IEditorGroupsAccessor, @@ -111,18 +119,21 @@ export class TabsTitleControl extends TitleControl { @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, @IEditorService private readonly editorService: EditorServiceImpl, @IPathService private readonly pathService: IPathService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, extensionService, configurationService, fileService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, configurationService, fileService); + // Resolve the correct path library for the OS we are on // If we are connected to remote, this accounts for the // remote OS. (async () => this.path = await this.pathService.path)(); + + // React to decorations changing for our resource labels + this._register(this.tabResourceLabels.onDidChangeDecorations(() => this.doHandleDecorationsChange())); } protected create(parent: HTMLElement): void { @@ -155,7 +166,7 @@ export class TabsTitleControl extends TitleControl { // Editor Actions Toolbar this.createEditorActionsToolBar(this.editorToolbarContainer); - // Breadcrumbs (are on a separate row below tabs and actions) + // Breadcrumbs const breadcrumbsContainer = document.createElement('div'); breadcrumbsContainer.classList.add('tabs-breadcrumbs'); this.titleContainer.appendChild(breadcrumbsContainer); @@ -193,7 +204,7 @@ export class TabsTitleControl extends TitleControl { } private updateBreadcrumbsControl(): void { - if (this.breadcrumbsControl && this.breadcrumbsControl.update()) { + if (this.breadcrumbsControl?.update()) { this.group.relayout(); // relayout when we have a breadcrumbs and when update changed its hidden-status } } @@ -246,7 +257,7 @@ export class TabsTitleControl extends TitleControl { }); // Prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) - this._register(addDisposableListener(tabsContainer, EventType.MOUSE_DOWN, (e: MouseEvent) => { + this._register(addDisposableListener(tabsContainer, EventType.MOUSE_DOWN, e => { if (e.button === 1) { e.preventDefault(); } @@ -341,8 +352,27 @@ export class TabsTitleControl extends TitleControl { } } - // Figure out scrolling direction - const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + (e.deltaX < 0 || e.deltaY < 0 /* scrolling up */ ? -1 : 1)); + // Ignore event if the last one happened too recently (https://github.com/microsoft/vscode/issues/96409) + // The restriction is relaxed according to the absolute value of `deltaX` and `deltaY` + // to support discrete (mouse wheel) and contiguous scrolling (touchpad) equally well + const now = Date.now(); + if (now - this.lastMouseWheelEventTime < TabsTitleControl.MOUSE_WHEEL_EVENT_THRESHOLD - 2 * (Math.abs(e.deltaX) + Math.abs(e.deltaY))) { + return; + } + + this.lastMouseWheelEventTime = now; + + // Figure out scrolling direction but ignore it if too subtle + let tabSwitchDirection: number; + if (e.deltaX + e.deltaY < - TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = -1; + } else if (e.deltaX + e.deltaY > TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = 1; + } else { + return; + } + + const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + tabSwitchDirection); if (!nextEditor) { return; } @@ -355,12 +385,19 @@ export class TabsTitleControl extends TitleControl { })); } + private doHandleDecorationsChange(): void { + + // A change to decorations potentially has an impact on the size of tabs + // so we need to trigger a layout in that case to adjust things + this.layout(this.dimensions); + } + protected updateEditorActionsToolbar(): void { super.updateEditorActionsToolbar(); // Changing the actions in the toolbar can have an impact on the size of the // tab container, so we need to layout the tabs to make sure the active is visible - this.layout(this.dimension); + this.layout(this.dimensions); } openEditor(editor: IEditorInput): void { @@ -443,7 +480,7 @@ export class TabsTitleControl extends TitleControl { }); // Moving an editor requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } pinEditor(editor: IEditorInput): void { @@ -470,7 +507,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to the sticky state requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } setActive(isGroupActive: boolean): void { @@ -482,7 +519,7 @@ export class TabsTitleControl extends TitleControl { // Activity has an impact on the toolbar, so we need to update and layout this.updateEditorActionsToolbar(); - this.layout(this.dimension); + this.layout(this.dimensions); } private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); @@ -508,7 +545,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to a label requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } updateEditorDirty(editor: IEditorInput): void { @@ -536,7 +573,8 @@ export class TabsTitleControl extends TitleControl { oldOptions.showIcons !== newOptions.showIcons || oldOptions.hasIcons !== newOptions.hasIcons || oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs || - !equals(oldOptions.tabDecorations, newOptions.tabDecorations) + oldOptions.wrapTabs !== newOptions.wrapTabs || + !equals(oldOptions.decorations, newOptions.decorations) ) { this.redraw(); } @@ -653,7 +691,7 @@ export class TabsTitleControl extends TitleControl { }; // Open on Click / Touch - disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, (e: MouseEvent) => handleClickOrTouch(e))); + disposables.add(addDisposableListener(tab, EventType.MOUSE_DOWN, e => handleClickOrTouch(e))); disposables.add(addDisposableListener(tab, TouchEventType.Tap, (e: GestureEvent) => handleClickOrTouch(e))); // Touch Scroll Support @@ -662,14 +700,14 @@ export class TabsTitleControl extends TitleControl { })); // Prevent flicker of focus outline on tab until editor got focus - disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, (e: MouseEvent) => { + disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, e => { EventHelper.stop(e); tab.blur(); })); // Close on mouse middle click - disposables.add(addDisposableListener(tab, EventType.AUXCLICK, (e: MouseEvent) => { + disposables.add(addDisposableListener(tab, EventType.AUXCLICK, e => { if (e.button === 1 /* Middle Button*/) { EventHelper.stop(e, true /* for https://github.com/microsoft/vscode/issues/56715 */); @@ -679,7 +717,7 @@ export class TabsTitleControl extends TitleControl { })); // Context menu on Shift+F10 - disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, (e: KeyboardEvent) => { + disposables.add(addDisposableListener(tab, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); if (event.shiftKey && event.keyCode === KeyCode.F10) { showContextMenu(e); @@ -692,7 +730,7 @@ export class TabsTitleControl extends TitleControl { })); // Keyboard accessibility - disposables.add(addDisposableListener(tab, EventType.KEY_UP, (e: KeyboardEvent) => { + disposables.add(addDisposableListener(tab, EventType.KEY_UP, e => { const event = new StandardKeyboardEvent(e); let handled = false; @@ -755,7 +793,7 @@ export class TabsTitleControl extends TitleControl { }); // Context menu - disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, (e: Event) => { + disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, e => { EventHelper.stop(e, true); const input = this.group.getEditorByIndex(index); @@ -765,7 +803,7 @@ export class TabsTitleControl extends TitleControl { }, true /* use capture to fix https://github.com/microsoft/vscode/issues/19145 */)); // Drag support - disposables.add(addDisposableListener(tab, EventType.DRAG_START, (e: DragEvent) => { + disposables.add(addDisposableListener(tab, EventType.DRAG_START, e => { const editor = this.group.getEditorByIndex(index); if (!editor) { return; @@ -1034,7 +1072,7 @@ export class TabsTitleControl extends TitleControl { this.updateEditorActionsToolbar(); // Ensure the active tab is always revealed - this.layout(this.dimension); + this.layout(this.dimensions); } private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { @@ -1104,12 +1142,14 @@ export class TabsTitleControl extends TitleControl { // or their first character of the name otherwise let name: string | undefined; let forceLabel = false; + let forceDisableBadgeDecorations = false; let description: string; if (options.pinnedTabSizing === 'compact' && this.group.isSticky(index)) { const isShowingIcons = options.showIcons && options.hasIcons; name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase(); description = ''; forceLabel = true; + forceDisableBadgeDecorations = true; // not enough space when sticky tabs are compact } else { name = tabLabel.name; description = tabLabel.description || ''; @@ -1128,7 +1168,16 @@ export class TabsTitleControl extends TitleControl { // Label tabLabelWidget.setResource( { name, description, resource: EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.BOTH }) }, - { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel, fileDecorations: { colors: Boolean(options.tabDecorations?.colors), badges: Boolean(options.tabDecorations?.badges) } } + { + title, + extraClasses: ['tab-label'], + italic: !this.group.isPinned(editor), + forceLabel, + fileDecorations: { + colors: Boolean(options.decorations?.colors), + badges: forceDisableBadgeDecorations ? false : Boolean(options.decorations?.badges) + } + } ); this.setEditorTabColor(editor, tabContainer, this.group.isActive(editor)); // {{SQL CARBON EDIT}} -- Display the editor's tab color @@ -1251,62 +1300,179 @@ export class TabsTitleControl extends TitleControl { } getDimensions(): IEditorGroupTitleDimensions { - let height = TabsTitleControl.TAB_HEIGHT; - if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - height += BreadcrumbsControl.HEIGHT; + let height: number; + + // Wrap: we need to ask `offsetHeight` to get + // the real height of the title area with wrapping. + if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + height = this.tabsAndActionsContainer.offsetHeight; + } else { + height = TabsTitleControl.TAB_HEIGHT; } - return { - height, - offset: TabsTitleControl.TAB_HEIGHT - }; + const offset = height; + + // Account for breadcrumbs if visible + if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + height += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible + } + + return { height, offset }; } - layout(dimension: Dimension | undefined): void { - this.dimension = dimension; + layout(dimensions: ITitleControlDimensions): Dimension { - const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex || !this.dimension) { - return; - } + // Remember dimensions that we get + Object.assign(this.dimensions, dimensions); // The layout of tabs can be an expensive operation because we access DOM properties // that can result in the browser doing a full page layout to validate them. To buffer // this a little bit we try at least to schedule this work on the next animation frame. if (!this.layoutScheduled.value) { this.layoutScheduled.value = scheduleAtNextAnimationFrame(() => { - const dimension = assertIsDefined(this.dimension); - this.doLayout(dimension); + this.doLayout(this.dimensions); this.layoutScheduled.clear(); }); } + + // First time layout: compute the dimensions and store it + if (!this.dimensions.used) { + this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); + } + + return this.dimensions.used; } - private doLayout(dimension: Dimension): void { + private doLayout(dimensions: ITitleControlDimensions): void { + + // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex) { - return; // nothing to do if not editor opened + if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { + + // Breadcrumbs + this.doLayoutBreadcrumbs(dimensions); + + // Tabs + const [activeTab, activeIndex] = activeTabAndIndex; + this.doLayoutTabs(activeTab, activeIndex, dimensions); } - // Breadcrumbs - this.doLayoutBreadcrumbs(dimension); + // Remember the dimensions used in the control so that we can + // return it fast from the `layout` call without having to + // compute it over and over again + const oldDimension = this.dimensions.used; + const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); - // Tabs - const [activeTab, activeIndex] = activeTabAndIndex; - this.doLayoutTabs(activeTab, activeIndex); + // In case the height of the title control changed from before + // (currently only possible if wrapping changed on/off), we need + // to signal this to the outside via a `relayout` call so that + // e.g. the editor control can be adjusted accordingly. + if (oldDimension && oldDimension.height !== newDimension.height) { + this.group.relayout(); + } } - private doLayoutBreadcrumbs(dimension: Dimension): void { + private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - const tabsScrollbar = assertIsDefined(this.tabsScrollbar); - - this.breadcrumbsControl.layout(new Dimension(dimension.width, BreadcrumbsControl.HEIGHT)); - tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + this.breadcrumbsControl.layout(new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT)); } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void { + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions): void { + + // Always first layout tabs with wrapping support even if wrapping + // is disabled. The result indicates if tabs wrap and if not, we + // need to proceed with the layout without wrapping because even + // if wrapping is enabled in settings, there are cases where + // wrapping is disabled (e.g. due to space constraints) + const tabsWrapMultiLine = this.doLayoutTabsWrapping(dimensions); + if (!tabsWrapMultiLine) { + this.doLayoutTabsNonWrapping(activeTab, activeIndex); + } + } + + private doLayoutTabsWrapping(dimensions: ITitleControlDimensions): boolean { + const [tabsAndActionsContainer, tabsContainer, editorToolbarContainer, tabsScrollbar] = assertAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.editorToolbarContainer, this.tabsScrollbar); + + // Handle wrapping tabs according to setting: + // - enabled: only add class if tabs wrap and don't exceed available dimensions + // - disabled: remove class and margin-right variable + + const didTabsWrapMultiLine = tabsAndActionsContainer.classList.contains('wrapping'); + let tabsWrapMultiLine = didTabsWrapMultiLine; + + function updateTabsWrapping(enabled: boolean): void { + tabsWrapMultiLine = enabled; + + // Toggle the `wrapped` class to enable wrapping + tabsAndActionsContainer.classList.toggle('wrapping', tabsWrapMultiLine); + + // Update `last-tab-margin-right` CSS variable to account for the absolute + // positioned editor actions container when tabs wrap. The margin needs to + // be the width of the editor actions container to avoid screen cheese. + tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0'); + } + + // Setting enabled: selectively enable wrapping if possible + if (this.accessor.partOptions.wrapTabs) { + const visibleTabsWidth = tabsContainer.offsetWidth; + const allTabsWidth = tabsContainer.scrollWidth; + const lastTabFitsWrapped = () => { + const lastTab = this.getLastTab(); + if (!lastTab) { + return true; // no tab always fits + } + + return lastTab.offsetWidth <= (dimensions.available.width - editorToolbarContainer.offsetWidth); + }; + + // If tabs wrap or should start to wrap (when width exceeds visible width) + // we must trigger `updateWrapping` to set the `last-tab-margin-right` + // accordingly based on the number of actions. The margin is important to + // properly position the last tab apart from the actions + // + // We already check here if the last tab would fit when wrapped given the + // editor toolbar will also show right next to it. This ensures we are not + // enabling wrapping only to disable it again in the code below (this fixes + // flickering issue https://github.com/microsoft/vscode/issues/115050) + if (tabsWrapMultiLine || (allTabsWidth > visibleTabsWidth && lastTabFitsWrapped())) { + updateTabsWrapping(true); + } + + // Tabs wrap multiline: remove wrapping under certain size constraint conditions + if (tabsWrapMultiLine) { + if ( + (tabsContainer.offsetHeight > dimensions.available.height) || // if height exceeds available height + (allTabsWidth === visibleTabsWidth && tabsContainer.offsetHeight === TabsTitleControl.TAB_HEIGHT) || // if wrapping is not needed anymore + (!lastTabFitsWrapped()) // if last tab does not fit anymore + ) { + updateTabsWrapping(false); + } + } + } + + // Setting disabled: remove CSS traces only if tabs did wrap + else if (didTabsWrapMultiLine) { + updateTabsWrapping(false); + } + + // If we transitioned from non-wrapping to wrapping, we need + // to update the scrollbar to have an equal `width` and + // `scrollWidth`. Otherwise a scrollbar would appear which is + // never desired when wrapping. + if (tabsWrapMultiLine && !didTabsWrapMultiLine) { + const visibleTabsWidth = tabsContainer.offsetWidth; + tabsScrollbar.setScrollDimensions({ + width: visibleTabsWidth, + scrollWidth: visibleTabsWidth + }); + } + + return tabsWrapMultiLine; + } + + private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1325,7 +1491,7 @@ export class TabsTitleControl extends TitleControl { // [-- Sticky Tabs Width --] // - const visibleTabsContainerWidth = tabsContainer.offsetWidth; + const visibleTabsWidth = tabsContainer.offsetWidth; const allTabsWidth = tabsContainer.scrollWidth; // Compute width of sticky tabs depending on pinned tab sizing @@ -1348,17 +1514,17 @@ export class TabsTitleControl extends TitleControl { } // Figure out if active tab is positioned static which has an - // impact on wether to reveal the tab or not later + // impact on whether to reveal the tab or not later let activeTabPositionStatic = this.accessor.partOptions.pinnedTabSizing !== 'normal' && this.group.isSticky(activeIndex); // Special case: we have sticky tabs but the available space for showing tabs // is little enough that we need to disable sticky tabs sticky positioning // so that tabs can be scrolled at naturally. - let availableTabsContainerWidth = visibleTabsContainerWidth - stickyTabsWidth; + let availableTabsContainerWidth = visibleTabsWidth - stickyTabsWidth; if (this.group.stickyCount > 0 && availableTabsContainerWidth < TabsTitleControl.TAB_WIDTH.fit) { tabsContainer.classList.add('disable-sticky-tabs'); - availableTabsContainerWidth = visibleTabsContainerWidth; + availableTabsContainerWidth = visibleTabsWidth; stickyTabsWidth = 0; activeTabPositionStatic = false; } else { @@ -1375,7 +1541,7 @@ export class TabsTitleControl extends TitleControl { // Update scrollbar tabsScrollbar.setScrollDimensions({ - width: visibleTabsContainerWidth, + width: visibleTabsWidth, scrollWidth: allTabsWidth }); @@ -1443,15 +1609,28 @@ export class TabsTitleControl extends TitleControl { private getTabAndIndex(editor: IEditorInput): [HTMLElement, number /* index */] | undefined { const editorIndex = this.group.getIndexOfEditor(editor); - if (editorIndex >= 0) { - const tabsContainer = assertIsDefined(this.tabsContainer); - - return [tabsContainer.children[editorIndex] as HTMLElement, editorIndex]; + const tab = this.getTabAtIndex(editorIndex); + if (tab) { + return [tab, editorIndex]; } return undefined; } + private getTabAtIndex(editorIndex: number): HTMLElement | undefined { + if (editorIndex >= 0) { + const tabsContainer = assertIsDefined(this.tabsContainer); + + return tabsContainer.children[editorIndex] as HTMLElement | undefined; + } + + return undefined; + } + + private getLastTab(): HTMLElement | undefined { + return this.getTabAtIndex(this.group.count - 1); + } + private blockRevealActiveTabOnce(): void { // When closing tabs through the tab close button or gesture, the user @@ -1540,7 +1719,7 @@ export class TabsTitleControl extends TitleControl { // {{SQL CARBON EDIT}} -- Display the editor's tab color private setEditorTabColor(editor: IEditorInput, tabContainer: HTMLElement, isTabActive: boolean) { let sqlEditor = editor as any; - const tabColorMode = this.configurationService.getValue('queryEditor').tabColorMode; + const tabColorMode = this.configurationService.getValue('queryEditor')?.tabColorMode ?? 'off'; if (tabColorMode === 'off' || (tabColorMode !== 'border' && tabColorMode !== 'fill') || this.themeService.getColorTheme().type === ColorScheme.HIGH_CONTRAST || !sqlEditor.tabColor) { tabContainer.style.borderTopColor = ''; @@ -1566,16 +1745,28 @@ export class TabsTitleControl extends TitleControl { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Add border between tabs and breadcrumbs in high contrast mode. if (theme.type === ColorScheme.HIGH_CONTRAST) { const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); + if (borderColor) { + collector.addRule(` + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { + border-bottom: 1px solid ${borderColor}; + } + `); + } + } + + // Add bottom border to tabs when wrapping + const borderColor = theme.getColor(TAB_BORDER); + if (borderColor) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { - border-bottom: 1px solid ${borderColor}; - } - `); + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab { + border-bottom: 1px solid ${borderColor}; + } + `); } // Styling with Outline color (e.g. high contrast theme) diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index c0d6111d32..7a9f047d5c 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as objects from 'vs/base/common/objects'; -import { isFunction, isObject, isArray, assertIsDefined } from 'vs/base/common/types'; +import { isFunction, isObject, isArray, assertIsDefined, withUndefinedAsNull } from 'vs/base/common/types'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -110,7 +110,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { if (this.reverseColor) { (configuration as IDiffEditorOptions).reverse = true; } // {{SQL CARBON EDIT}} - return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); + return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}); } async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -139,8 +139,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan // Set Editor Model const diffEditor = assertIsDefined(this.getControl()); - const resolvedDiffEditorModel = resolvedModel; - diffEditor.setModel(resolvedDiffEditorModel.textDiffEditorModel); + const resolvedDiffEditorModel = resolvedModel as TextDiffEditorModel; + diffEditor.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); // Apply Options from TextOptions let optionsGotApplied = false; diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index a6095641a9..2756458ed7 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -233,7 +233,6 @@ export abstract class BaseTextEditor extends EditorPane implements ITextEditorPa return undefined; } - protected saveTextEditorViewState(resource: URI, cleanUpOnDispose?: IEditorInput): void { const editorViewState = this.retrieveTextEditorViewState(resource); if (!editorViewState || !this.group) { diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index c43e702bb9..08acc31c56 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -9,14 +9,13 @@ import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; -import * as arrays from 'vs/base/common/arrays'; +import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, IActionViewItem } from 'vs/base/common/actions'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; -import { createAndFillInActionBarActions, createAndFillInContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ExecuteCommandAction, IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -26,7 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; @@ -34,7 +33,6 @@ import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/bro import { IEditorGroupsAccessor, IEditorGroupTitleDimensions, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, EditorResourceAccessor, IEditorPartOptions, SideBySideEditor, ActiveEditorPinnedContext, ActiveEditorStickyContext } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types'; @@ -46,6 +44,20 @@ export interface IToolbarActions { secondary: IAction[]; } +export interface ITitleControlDimensions { + + /** + * The size of the parent container the title control is layed out in. + */ + container: Dimension; + + /** + * The maximum size the title control is allowed to consume based on + * other controls that are positioned inside the container. + */ + available: Dimension; +} + export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -53,9 +65,6 @@ export abstract class TitleControl extends Themable { protected breadcrumbsControl: BreadcrumbsControl | undefined = undefined; - private currentPrimaryEditorActionIds: string[] = []; - private currentSecondaryEditorActionIds: string[] = []; - private editorActionsToolbar: ToolBar | undefined; private resourceContext: ResourceContextKey; @@ -80,7 +89,6 @@ export abstract class TitleControl extends Themable { @IMenuService private readonly menuService: IMenuService, @IQuickInputService protected quickInputService: IQuickInputService, @IThemeService themeService: IThemeService, - @IExtensionService private readonly extensionService: IExtensionService, @IConfigurationService protected configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService ) { @@ -93,13 +101,6 @@ export abstract class TitleControl extends Themable { this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService)); this.create(parent); - this.registerListeners(); - } - - protected registerListeners(): void { - - // Update actions toolbar when extension register that may contribute them - this._register(this.extensionService.onDidRegisterExtensions(() => this.updateEditorActionsToolbar())); } protected abstract create(parent: HTMLElement): void; @@ -124,7 +125,7 @@ export abstract class TitleControl extends Themable { } this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => { - if (this.breadcrumbsControl && this.breadcrumbsControl.update()) { + if (this.breadcrumbsControl?.update()) { this.handleBreadcrumbsEnablementChange(); } })); @@ -148,7 +149,7 @@ export abstract class TitleControl extends Themable { this.editorActionsToolbar.context = context; // Action Run Handling - this._register(this.editorActionsToolbar.actionRunner.onDidRun((e: IRunEvent) => { + this._register(this.editorActionsToolbar.actionRunner.onDidRun(e => { // Notify for Error this.notificationService.error(e.error); @@ -173,35 +174,14 @@ export abstract class TitleControl extends Themable { } // Check extensions - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; + return createActionViewItem(this.instantiationService, action); } protected updateEditorActionsToolbar(): void { - - // Update Editor Actions Toolbar const { primaryEditorActions, secondaryEditorActions } = this.prepareEditorActions(this.getEditorActions()); - // Only update if something actually has changed - const primaryEditorActionIds = primaryEditorActions.map(a => a.id); - const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id); - if ( - !arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) || - !arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds) || - primaryEditorActions.some(action => action instanceof ExecuteCommandAction) || // execute command actions can have the same ID but different arguments - secondaryEditorActions.some(action => action instanceof ExecuteCommandAction) // see also https://github.com/microsoft/vscode/issues/16298 - ) { - const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); - editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); - - this.currentPrimaryEditorActionIds = primaryEditorActionIds; - this.currentSecondaryEditorActionIds = secondaryEditorActionIds; - } + const editorActionsToolbar = assertIsDefined(this.editorActionsToolbar); + editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions); } protected prepareEditorActions(editorActions: IToolbarActions): { primaryEditorActions: IAction[]; secondaryEditorActions: IAction[]; } { @@ -245,25 +225,20 @@ export abstract class TitleControl extends Themable { this.updateEditorActionsToolbar(); // Update editor toolbar whenever contributed actions change })); - this.editorToolBarMenuDisposables.add(createAndFillInActionBarActions(titleBarMenu, { arg: this.resourceContext.get(), shouldForwardArgs: true }, { primary, secondary }, (group: string) => group === 'navigation' || group === '1_run')); + this.editorToolBarMenuDisposables.add(createAndFillInActionBarActions(titleBarMenu, { arg: this.resourceContext.get(), shouldForwardArgs: true }, { primary, secondary }, (group: string) => group === 'navigation' || group === '1_run', 7)); } return { primary, secondary }; } protected clearEditorActionsToolbar(): void { - if (this.editorActionsToolbar) { - this.editorActionsToolbar.setActions([], []); - } - - this.currentPrimaryEditorActionIds = []; - this.currentSecondaryEditorActionIds = []; + this.editorActionsToolbar?.setActions([], []); } protected enableGroupDragging(element: HTMLElement): void { // Drag start - this._register(addDisposableListener(element, EventType.DRAG_START, (e: DragEvent) => { + this._register(addDisposableListener(element, EventType.DRAG_START, e => { if (e.target !== element) { return; // only if originating from tabs container } @@ -355,7 +330,7 @@ export abstract class TitleControl extends Themable { getAnchor: () => anchor, getActions: () => actions, getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) }), - getKeyBinding: (action) => this.getKeybinding(action), + getKeyBinding: action => this.getKeybinding(action), onHide: () => { // restore previous contexts @@ -408,7 +383,7 @@ export abstract class TitleControl extends Themable { abstract updateStyles(): void; - abstract layout(dimension: Dimension): void; + abstract layout(dimensions: ITitleControlDimensions): Dimension; abstract getDimensions(): IEditorGroupTitleDimensions; @@ -420,7 +395,7 @@ export abstract class TitleControl extends Themable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Drag Feedback const dragImageBackground = theme.getColor(listActiveSelectionBackground); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 1165784fbd..c0d4f29067 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/notificationsCenter'; import 'vs/css!./media/notificationsActions'; import { NOTIFICATIONS_BORDER, NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from 'vs/workbench/common/theme'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { Emitter } from 'vs/base/common/event'; @@ -313,7 +313,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const notificationBorderColor = theme.getColor(NOTIFICATIONS_BORDER); if (notificationBorderColor) { collector.addRule(`.monaco-workbench > .notifications-center .notifications-list-container .monaco-list-row[data-last-element="false"] > .notification-list-item { border-bottom: 1px solid ${notificationBorderColor}; }`); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 530dd12dc1..bb631d788d 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -10,7 +10,7 @@ import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IListOptions } from 'vs/base/browser/ui/list/listWidget'; import { NOTIFICATIONS_LINKS, NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_FOREGROUND, NOTIFICATIONS_ERROR_ICON_FOREGROUND, NOTIFICATIONS_WARNING_ICON_FOREGROUND, NOTIFICATIONS_INFO_ICON_FOREGROUND } from 'vs/workbench/common/theme'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, Themable } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { INotificationViewItem } from 'vs/workbench/common/notifications'; import { NotificationsListDelegate, NotificationRenderer } from 'vs/workbench/browser/parts/notifications/notificationsViewer'; @@ -278,7 +278,7 @@ export class NotificationsList extends Themable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const linkColor = theme.getColor(NOTIFICATIONS_LINKS); if (linkColor) { collector.addRule(`.monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-message a { color: ${linkColor}; }`); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index c05b529a36..6bb8947370 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -470,7 +470,9 @@ export class NotificationTemplateRenderer extends Disposable { : buttonToolbar.addButton(buttonOptions)); button.label = action.label; this.inputDisposables.add(button.onDidClick(e => { - EventHelper.stop(e, true); + if (e) { + EventHelper.stop(e, true); + } actionRunner.run(action); })); diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index e128485df0..454707c26e 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -5,45 +5,26 @@ import 'vs/css!./media/panelpart'; import * as nls from 'vs/nls'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Action } from 'vs/base/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IWorkbenchLayoutService, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { ActivityAction, ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositeBarActions'; import { IActivity } from 'vs/workbench/common/activity'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ActivePanelContext, PanelPositionContext } from 'vs/workbench/common/panel'; -import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { ActivePanelContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ViewContainerLocationToString, ViewContainerLocation } from 'vs/workbench/common/views'; const maximizeIcon = registerIcon('panel-maximize', Codicon.chevronUp, nls.localize('maximizeIcon', 'Icon to maximize a panel.')); const restoreIcon = registerIcon('panel-restore', Codicon.chevronDown, nls.localize('restoreIcon', 'Icon to restore a panel.')); const closeIcon = registerIcon('panel-close', Codicon.close, nls.localize('closeIcon', 'Icon to close a panel.')); -export class ClosePanelAction extends Action { - - static readonly ID = 'workbench.action.closePanel'; - static readonly LABEL = nls.localize('closePanel', "Close Panel"); - - constructor( - id: string, - name: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, name, ThemeIcon.asClassName(closeIcon)); - } - - async run(): Promise { - this.layoutService.setPanelHidden(true); - } -} - export class TogglePanelAction extends Action { static readonly ID = 'workbench.action.togglePanel'; @@ -91,46 +72,6 @@ class FocusPanelAction extends Action { } } - -export class ToggleMaximizedPanelAction extends Action { - - static readonly ID = 'workbench.action.toggleMaximizedPanel'; - static readonly LABEL = nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"); - - private static readonly MAXIMIZE_LABEL = nls.localize('maximizePanel', "Maximize Panel Size"); - private static readonly RESTORE_LABEL = nls.localize('minimizePanel', "Restore Panel Size"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService - ) { - super(id, label, layoutService.isPanelMaximized() ? ThemeIcon.asClassName(restoreIcon) : ThemeIcon.asClassName(maximizeIcon)); - - this.toDispose.add(editorGroupsService.onDidLayout(() => { - const maximized = this.layoutService.isPanelMaximized(); - this.class = maximized ? ThemeIcon.asClassName(restoreIcon) : ThemeIcon.asClassName(maximizeIcon); - this.label = maximized ? ToggleMaximizedPanelAction.RESTORE_LABEL : ToggleMaximizedPanelAction.MAXIMIZE_LABEL; - })); - } - - async run(): Promise { - if (!this.layoutService.isVisible(Parts.PANEL_PART)) { - this.layoutService.setPanelHidden(false); - // If the panel is not already maximized, maximize it - if (!this.layoutService.isPanelMaximized()) { - this.layoutService.toggleMaximizedPanel(); - } - } - else { - this.layoutService.toggleMaximizedPanel(); - } - } -} - const PositionPanelActionId = { LEFT: 'workbench.action.positionPanelLeft', RIGHT: 'workbench.action.positionPanelRight', @@ -218,7 +159,6 @@ export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinne } } - export class SwitchPanelViewAction extends Action { constructor( @@ -287,35 +227,117 @@ export class NextPanelViewAction extends SwitchPanelViewAction { const actionRegistry = Registry.as(WorkbenchExtensions.WorkbenchActions); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(TogglePanelAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_J }), 'View: Toggle Panel', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPanelAction), 'View: Focus into Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMaximizedPanelAction), 'View: Toggle Maximized Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClosePanelAction), 'View: Close Panel', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(PreviousPanelViewAction), 'View: Previous Panel View', CATEGORIES.View.value); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NextPanelViewAction), 'View: Next Panel View', CATEGORIES.View.value); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '2_workbench_layout', - command: { - id: TogglePanelAction.ID, - title: nls.localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), - toggled: ActivePanelContext - }, - order: 5 +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.toggleMaximizedPanel', + title: { value: nls.localize('toggleMaximizedPanel', "Toggle Maximized Panel"), original: 'Toggle Maximized Panel' }, + tooltip: nls.localize('maximizePanel', "Maximize Panel Size"), + category: CATEGORIES.View, + f1: true, + icon: maximizeIcon, + toggled: { condition: PanelMaximizedContext, icon: restoreIcon, tooltip: nls.localize('minimizePanel', "Restore Panel Size") }, + menu: [{ + id: MenuId.PanelTitle, + group: 'navigation', + order: 1 + }] + }); + } + run(accessor: ServicesAccessor) { + const layoutService = accessor.get(IWorkbenchLayoutService); + if (!layoutService.isVisible(Parts.PANEL_PART)) { + layoutService.setPanelHidden(false); + // If the panel is not already maximized, maximize it + if (!layoutService.isPanelMaximized()) { + layoutService.toggleMaximizedPanel(); + } + } + else { + layoutService.toggleMaximizedPanel(); + } + } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.closePanel', + title: { value: nls.localize('closePanel', "Close Panel"), original: 'Close Panel' }, + category: CATEGORIES.View, + icon: closeIcon, + menu: [{ + id: MenuId.CommandPalette, + when: PanelVisibleContext, + }, { + id: MenuId.PanelTitle, + group: 'navigation', + order: 2 + }] + }); + } + run(accessor: ServicesAccessor) { + accessor.get(IWorkbenchLayoutService).setPanelHidden(true); + } +}); + +MenuRegistry.appendMenuItems([ + { + id: MenuId.MenubarAppearanceMenu, + item: { + group: '2_workbench_layout', + command: { + id: TogglePanelAction.ID, + title: nls.localize({ key: 'miShowPanel', comment: ['&& denotes a mnemonic'] }, "Show &&Panel"), + toggled: ActivePanelContext + }, + order: 5 + } + }, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: TogglePanelAction.ID, + title: { value: nls.localize('hidePanel', "Hide Panel"), original: 'Hide Panel' }, + }, + when: ContextKeyExpr.and(PanelVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), + order: 2 + } + } +]); + function registerPositionPanelActionById(config: PanelActionConfig) { const { id, label, alias, when } = config; // register the workbench action actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SetPanelPositionAction, id, label), alias, CATEGORIES.View.value, when); // register as a menu item - MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_workbench_layout_move', - command: { - id, - title: label - }, - when, - order: 5 - }); + MenuRegistry.appendMenuItems([{ + id: MenuId.MenubarAppearanceMenu, + item: { + group: '3_workbench_layout_move', + command: { + id, + title: label + }, + when, + order: 5 + } + }, { + id: MenuId.ViewTitleContext, + item: { + group: '3_workbench_layout_move', + command: { + id: id, + title: label, + }, + when: ContextKeyExpr.and(when, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Panel))), + order: 1 + } + }]); } // register each position panel action diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index f1dda93ce4..5402bfa48d 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/panelpart'; -import { IAction, Action } from 'vs/base/common/actions'; +import { localize } from 'vs/nls'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -18,8 +19,8 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { PanelActivityAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; @@ -27,15 +28,12 @@ import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/composit import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension, trackFocus, EventHelper } from 'vs/base/browser/dom'; -import { localize } from 'vs/nls'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ViewContainer, IViewDescriptorService, IViewContainerModel, ViewContainerLocation } from 'vs/workbench/common/views'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { ViewMenuActions, ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; +import { ViewContainer, IViewDescriptorService, IViewContainerModel, ViewContainerLocation, getEnabledViewContainerContextKey } from 'vs/workbench/common/views'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { Before2D, CompositeDragAndDropObserver, ICompositeDragAndDrop, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { IActivity } from 'vs/workbench/common/activity'; @@ -46,7 +44,7 @@ interface ICachedPanel { pinned: boolean; order?: number; visible: boolean; - views?: { when?: string }[]; + views?: { when?: string; }[]; } interface IPlaceholderViewContainer { @@ -105,6 +103,8 @@ export class PanelPart extends CompositePart implements IPanelService { private dndHandler: ICompositeDragAndDrop; + private readonly enabledViewContainersContextKeys: Map> = new Map>(); + constructor( @INotificationService notificationService: INotificationService, @IStorageService storageService: IStorageService, @@ -148,24 +148,27 @@ export class PanelPart extends CompositePart implements IPanelService { this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { icon: false, orientation: ActionsOrientation.HORIZONTAL, - openComposite: (compositeId: string) => this.openPanel(compositeId, true).then(panel => panel || null), - getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction, - getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, - getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), - getContextMenuActions: () => [ - ...PositionPanelActionConfigs - // show the contextual menu item if it is not in that position - .filter(({ when }) => contextKeyService.contextMatchesRules(when)) - .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), - this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) - ] as Action[], - getContextMenuActionsForComposite: (compositeId: string) => this.getContextMenuActionsForComposite(compositeId) as Action[], + openComposite: compositeId => this.openPanel(compositeId, true).then(panel => panel || null), + getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction, + getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction, + getOnCompositeClickAction: compositeId => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), + fillExtraContextMenuActions: actions => { + actions.push(...[ + new Separator(), + ...PositionPanelActionConfigs + // show the contextual menu item if it is not in that position + .filter(({ when }) => contextKeyService.contextMatchesRules(when)) + .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), + this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) + ]); + }, + getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId), getDefaultCompositeId: () => this.panelRegistry.getDefaultPanelId(), hidePart: () => this.layoutService.setPanelHidden(true), dndHandler: this.dndHandler, compositeSize: 0, overflowActionSize: 44, - colors: (theme: IColorTheme) => ({ + colors: theme => ({ activeBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action inactiveBackgroundColor: theme.getColor(PANEL_BACKGROUND), // Background color for overflow action activeBorderBottomColor: theme.getColor(PANEL_ACTIVE_TITLE_BORDER), @@ -184,20 +187,21 @@ export class PanelPart extends CompositePart implements IPanelService { this.onDidRegisterPanels([...this.getPanels()]); } - private getContextMenuActionsForComposite(compositeId: string): readonly IAction[] { + private getContextMenuActionsForComposite(compositeId: string): IAction[] { const result: IAction[] = []; - const container = this.getViewContainer(compositeId); - if (container) { - const viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!; + const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!; + if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) { + result.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) })); + } else { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); if (viewContainerModel.allViewDescriptors.length === 1) { - const viewMenuActions = this.instantiationService.createInstance(ViewMenuActions, viewContainerModel.allViewDescriptors[0].id, MenuId.ViewTitle, MenuId.ViewTitleContext); - result.push(...viewMenuActions.getContextMenuActions()); - viewMenuActions.dispose(); + const viewToReset = viewContainerModel.allViewDescriptors[0]; + const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!; + if (defaultContainer !== viewContainer) { + result.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) })); + } } - - const viewContainerMenuActions = this.instantiationService.createInstance(ViewContainerMenuActions, container.id, MenuId.ViewContainerTitleContext); - result.push(...viewContainerMenuActions.getContextMenuActions()); - viewContainerMenuActions.dispose(); } return result; } @@ -243,10 +247,10 @@ export class PanelPart extends CompositePart implements IPanelService { const viewContainer = this.getViewContainer(panel.id)!; const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); this.updateActivity(viewContainer, viewContainerModel); - this.onDidChangeActiveViews(viewContainer, viewContainerModel); + this.showOrHideViewContainer(viewContainer, viewContainerModel); const disposables = new DisposableStore(); - disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel))); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.showOrHideViewContainer(viewContainer, viewContainerModel))); disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel))); this.panelDisposables.set(panel.id, disposables); @@ -282,7 +286,7 @@ export class PanelPart extends CompositePart implements IPanelService { const activity: IActivity = { id: viewContainer.id, name: this.extensionsRegistered || cachedTitle === undefined ? viewContainerModel.title : cachedTitle, - keybindingId: viewContainer.focusCommand?.id + keybindingId: viewContainerModel.keybindingId }; const { activityAction, pinnedAction } = this.getCompositeActions(viewContainer.id); @@ -298,10 +302,17 @@ export class PanelPart extends CompositePart implements IPanelService { } } - private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + private showOrHideViewContainer(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + let contextKey = this.enabledViewContainersContextKeys.get(viewContainer.id); + if (!contextKey) { + contextKey = this.contextKeyService.createKey(getEnabledViewContainerContextKey(viewContainer.id), false); + this.enabledViewContainersContextKeys.set(viewContainer.id, contextKey); + } if (viewContainerModel.activeViewDescriptors.length) { - this.compositeBar.addComposite(viewContainer); + contextKey.set(true); + this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); } else if (viewContainer.hideIfEmpty) { + contextKey.set(false); this.hideComposite(viewContainer.id); } } @@ -534,13 +545,6 @@ export class PanelPart extends CompositePart implements IPanelService { .sort((p1, p2) => pinnedCompositeIds.indexOf(p1.id) - pinnedCompositeIds.indexOf(p2.id)); } - protected getActions(): ReadonlyArray { - return [ - this.instantiationService.createInstance(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL), - this.instantiationService.createInstance(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL) - ]; - } - getActivePanel(): IPanel | undefined { return this.getActiveComposite(); } @@ -803,7 +807,7 @@ export class PanelPart extends CompositePart implements IPanelService { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Panel Background: since panels can host editors, we apply a background rule if the panel background // color is different from the editor background color. This is a bit of a hack though. The better way diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 676b287f72..9615bfafcf 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -3,11 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Removed to allow progress bar positioning to escape */ -/* .monaco-workbench .sidebar > .content { - overflow: hidden; -} */ - .monaco-workbench.nosidebar > .part.sidebar { display: none !important; visibility: hidden !important; diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 9e7e0466e8..0ef28b7ec4 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -6,7 +6,6 @@ import 'vs/css!./media/sidebarpart'; import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action } from 'vs/base/common/actions'; import { CompositePart } from 'vs/workbench/browser/parts/compositePart'; import { Viewlet, ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -118,6 +117,9 @@ export class SidebarPart extends CompositePart implements IViewletServi { hasTitle: true, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 } ); + // let t = viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar); + // console.log(t.id); + this.registerListeners(); } @@ -288,8 +290,8 @@ export class SidebarPart extends CompositePart implements IViewletServi const anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => contextMenuActions, - getActionViewItem: action => this.actionViewItemProvider(action as Action), + getActions: () => contextMenuActions.slice(), + getActionViewItem: action => this.actionViewItemProvider(action), actionRunner: activeViewlet.getActionRunner() }); } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index f5392e413e..9edfd65a18 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { dispose, IDisposable, Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Part } from 'vs/workbench/browser/part'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,13 +15,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action, IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ThemeColor } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; -import { Color } from 'vs/base/common/color'; -import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, appendChildren } from 'vs/base/browser/dom'; +import { EventHelper, createStyleSheet, addDisposableListener, EventType, hide, show, isAncestor, append } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -38,7 +37,8 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { renderCodicon, renderCodicons } from 'vs/base/browser/codicons'; +import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { syncing } from 'vs/platform/theme/common/iconRegistry'; interface IPendingStatusbarEntry { id: string; @@ -610,7 +610,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } private getContextMenuActions(event: StandardMouseEvent): IAction[] { - const actions: Action[] = []; + const actions: IAction[] = []; // Provide an action to hide the status bar at last actions.push(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, nls.localize('hideStatusBar', "Hide Status Bar"))); @@ -704,9 +704,9 @@ export class StatusbarPart extends Part implements IStatusbarService { } } -class StatusBarCodiconLabel extends CodiconLabel { +class StatusBarCodiconLabel extends SimpleIconLabel { - private readonly progressCodicon = renderCodicon('sync', 'spin'); + private readonly progressCodicon: any = renderIcon(syncing); private currentText = ''; private currentShowProgress = false; @@ -749,7 +749,7 @@ class StatusBarCodiconLabel extends CodiconLabel { } // Append new elements - appendChildren(this.container, ...renderCodicons(textContent)); + append(this.container, ...renderLabelWithIcons(textContent)); } // No Progress: no special handling @@ -893,7 +893,7 @@ class StatusbarEntryItem extends Disposable { } private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): void { - let colorResult: string | null = null; + let colorResult: string | undefined = undefined; if (isBackground) { this.backgroundListener.clear(); @@ -903,15 +903,15 @@ class StatusbarEntryItem extends Disposable { if (color) { if (isThemeColor(color)) { - colorResult = (this.themeService.getColorTheme().getColor(color.id) || Color.transparent).toString(); + colorResult = this.themeService.getColorTheme().getColor(color.id)?.toString(); const listener = this.themeService.onDidColorThemeChange(theme => { - const colorValue = (theme.getColor(color.id) || Color.transparent).toString(); + const colorValue = theme.getColor(color.id)?.toString(); if (isBackground) { - container.style.backgroundColor = colorValue; + container.style.backgroundColor = colorValue ?? ''; } else { - container.style.color = colorValue; + container.style.color = colorValue ?? ''; } }); @@ -926,9 +926,9 @@ class StatusbarEntryItem extends Disposable { } if (isBackground) { - container.style.backgroundColor = colorResult || ''; + container.style.backgroundColor = colorResult ?? ''; } else { - container.style.color = colorResult || ''; + container.style.color = colorResult ?? ''; } } @@ -942,7 +942,7 @@ class StatusbarEntryItem extends Disposable { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { if (theme.type !== ColorScheme.HIGH_CONTRAST) { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index db6e307625..4fd4fdf402 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -85,11 +85,34 @@ height: 100%; position: relative; z-index: 3000; + flex-shrink: 0; +} + +.monaco-workbench .part.titlebar > .window-appicon:not(.codicon) { background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; background-size: 16px; - flex-shrink: 0; +} + +.monaco-workbench .part.titlebar .window-appicon > .home-bar-icon-badge { + position: absolute; + right: 9px; + bottom: 6px; + width: 8px; + height: 8px; + z-index: 1; /* on top of home indicator */ + background-image: url('../../../media/code-icon.svg'); + background-repeat: no-repeat; + background-position: center center; + background-size: 8px; + pointer-events: none; + border-top: 1px solid transparent; + border-left: 1px solid transparent; +} + +.monaco-workbench .part.titlebar > .window-appicon.codicon { + line-height: 30px; } .monaco-workbench.fullscreen .part.titlebar > .window-appicon { diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 4d71bc1798..b3263771a2 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -37,6 +37,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export abstract class MenubarControl extends Disposable { @@ -91,7 +92,8 @@ export abstract class MenubarControl extends Disposable { protected readonly preferencesService: IPreferencesService, protected readonly environmentService: IWorkbenchEnvironmentService, protected readonly accessibilityService: IAccessibilityService, - protected readonly hostService: IHostService + protected readonly hostService: IHostService, + protected readonly commandService: ICommandService ) { super(); @@ -200,13 +202,27 @@ export abstract class MenubarControl extends Disposable { if (event.affectsConfiguration('editor.accessibilitySupport')) { this.notifyUserOfCustomMenubarAccessibility(); } + + // 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(); + } + } + + private get menubarHidden(): boolean { + return isMacintosh && isNative ? false : getMenuBarVisibility(this.configurationService) === 'hidden'; } protected onRecentlyOpenedChange(): void { - this.workspacesService.getRecentlyOpened().then(recentlyOpened => { - this.recentlyOpened = recentlyOpened; - this.updateMenubar(); - }); + + // Do not update recently opened when the menubar is hidden #108712 + if (!this.menubarHidden) { + this.workspacesService.getRecentlyOpened().then(recentlyOpened => { + this.recentlyOpened = recentlyOpened; + this.updateMenubar(); + }); + } } private createOpenRecentMenuAction(recent: IRecent): IAction & { uri: URI } { @@ -295,23 +311,10 @@ export class CustomMenubarControl extends MenubarControl { @IAccessibilityService accessibilityService: IAccessibilityService, @IThemeService private readonly themeService: IThemeService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @ICommandService commandService: ICommandService ) { - super( - menuService, - workspacesService, - contextKeyService, - keybindingService, - configurationService, - labelService, - updateService, - storageService, - notificationService, - preferencesService, - environmentService, - accessibilityService, - hostService - ); + super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService); this._onVisibilityChange = this._register(new Emitter()); this._onFocusStateChange = this._register(new Emitter()); @@ -324,7 +327,17 @@ export class CustomMenubarControl extends MenubarControl { this.registerActions(); - registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + // Register web menu actions to the file menu when its in the title + this.getWebNavigationMenuItemActions().forEach(actionItem => { + this._register(MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + command: actionItem.item, + title: actionItem.item.title, + group: 'z_Web', + when: ContextKeyExpr.and(IsWebContext, ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) + })); + }); + + registerThemingParticipant((theme, collector) => { const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (menubarActiveWindowFgColor) { collector.addRule(` @@ -343,7 +356,6 @@ export class CustomMenubarControl extends MenubarControl { color: ${activityBarInactiveFgColor}; } `); - } const activityBarFgColor = theme.getColor(ACTIVITY_BAR_FOREGROUND); @@ -370,7 +382,6 @@ export class CustomMenubarControl extends MenubarControl { `); } - const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND); if (menubarSelectedFgColor) { collector.addRule(` @@ -595,6 +606,12 @@ export class CustomMenubarControl extends MenubarControl { for (let action of actions) { this.insertActionsBefore(action, target); + + // use mnemonicTitle whenever possible + const title = typeof action.item.title === 'string' + ? action.item.title + : action.item.title.mnemonicTitle ?? action.item.title.value; + if (action instanceof SubmenuItemAction) { let submenu = this.menus[action.item.submenu.id]; if (!submenu) { @@ -612,10 +629,12 @@ export class CustomMenubarControl extends MenubarControl { const submenuActions: SubmenuAction[] = []; updateActions(submenu, submenuActions, topLevelTitle); - target.push(new SubmenuAction(action.id, mnemonicMenuLabel(action.label), submenuActions)); + target.push(new SubmenuAction(action.id, mnemonicMenuLabel(title), submenuActions)); } else { - action.label = mnemonicMenuLabel(this.calculateActionLabel(action)); - target.push(action); + const newAction = new Action(action.id, mnemonicMenuLabel(title), action.class, action.enabled, () => this.commandService.executeCommand(action.id)); + newAction.tooltip = action.tooltip; + newAction.checked = action.checked; + target.push(newAction); } } @@ -654,6 +673,27 @@ export class CustomMenubarControl extends MenubarControl { } } + private getWebNavigationMenuItemActions(): MenuItemAction[] { + if (!isWeb) { + return []; // only for web + } + + const webNavigationActions = []; + const webNavigationMenu = this.menuService.createMenu(MenuId.MenubarHomeMenu, this.contextKeyService); + for (const groups of webNavigationMenu.getActions()) { + const [, actions] = groups; + for (const action of actions) { + if (action instanceof MenuItemAction) { + webNavigationActions.push(action); + } + } + } + + webNavigationMenu.dispose(); + + return webNavigationActions; + } + private getMenuBarOptions(): IMenuBarOptions { return { enableMnemonics: this.currentEnableMenuBarMnemonics, @@ -661,7 +701,35 @@ export class CustomMenubarControl extends MenubarControl { visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id), alwaysOnMnemonics: this.alwaysOnMnemonics, - compactMode: this.currentCompactMenuMode + compactMode: this.currentCompactMenuMode, + getCompactMenuActions: () => { + if (!isWeb) { + return []; // only for web + } + + const webNavigationActions: IAction[] = []; + const href = this.environmentService.options?.homeIndicator?.href; + if (href) { + webNavigationActions.push(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, + async (event?: MouseEvent) => { + if ((!isMacintosh && event?.ctrlKey) || (isMacintosh && event?.metaKey)) { + window.open(href, '_blank'); + } else { + window.location.href = href; + } + })); + } + + const otherActions = this.getWebNavigationMenuItemActions().map(action => { + const title = typeof action.item.title === 'string' + ? action.item.title + : action.item.title.mnemonicTitle ?? action.item.title.value; + return new Action(action.id, mnemonicMenuLabel(title), action.class, action.enabled, () => this.commandService.executeCommand(action.id)); + }); + + webNavigationActions.push(...otherActions); + return webNavigationActions; + } }; } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 9a6ade4110..77dc62a34e 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/titlebarpart'; +import { localize } from 'vs/nls'; import { dirname, basename } from 'vs/base/common/resources'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; @@ -15,17 +16,16 @@ import { IAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import * as nls from 'vs/nls'; import { EditorResourceAccessor, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template } from 'vs/base/common/labels'; @@ -41,12 +41,13 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { Codicon, iconRegistry } from 'vs/base/common/codicons'; export class TitlebarPart extends Part implements ITitleService { - private static readonly NLS_UNSUPPORTED = nls.localize('patchedWindowTitle', "[Unsupported]"); - private static readonly NLS_USER_IS_ADMIN = isWindows ? nls.localize('userIsAdmin', "[Administrator]") : nls.localize('userIsSudo', "[Superuser]"); - private static readonly NLS_EXTENSION_HOST = nls.localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); + private static readonly NLS_UNSUPPORTED = localize('patchedWindowTitle', "[Unsupported]"); + private static readonly NLS_USER_IS_ADMIN = isWindows ? localize('userIsAdmin', "[Administrator]") : localize('userIsSudo', "[Superuser]"); + private static readonly NLS_EXTENSION_HOST = localize('devExtensionWindowTitlePrefix', "[Extension Development Host]"); private static readonly TITLE_DIRTY = '\u25cf '; //#region IView @@ -65,6 +66,8 @@ export class TitlebarPart extends Part implements ITitleService { protected title!: HTMLElement; protected customMenubar: CustomMenubarControl | undefined; + protected appIcon: HTMLElement | undefined; + private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; protected lastLayoutDimensions: Dimension | undefined; private titleBarStyle: 'native' | 'custom'; @@ -86,7 +89,7 @@ export class TitlebarPart extends Part implements ITitleService { @IEditorService private readonly editorService: IEditorService, @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @ILabelService private readonly labelService: ILabelService, @IStorageService storageService: IStorageService, @@ -341,6 +344,28 @@ export class TitlebarPart extends Part implements ITitleService { createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; + // App Icon (Native Windows/Linux and Web) + if (!isMacintosh || isWeb) { + this.appIcon = prepend(this.element, $('a.window-appicon')); + + // Web-only home indicator and menu + if (isWeb) { + const homeIndicator = this.environmentService.options?.homeIndicator; + if (homeIndicator) { + let codicon = iconRegistry.get(homeIndicator.icon); + if (!codicon) { + codicon = Codicon.code; + } + + this.appIcon.setAttribute('href', homeIndicator.href); + this.appIcon.classList.add(...codicon.classNamesArray); + this.appIconBadge = document.createElement('div'); + this.appIconBadge.classList.add('home-bar-icon-badge'); + this.appIcon.appendChild(this.appIconBadge); + } + } + } + // Menubar: install a custom menu bar depending on configuration // and when not in activity bar if (this.titleBarStyle !== 'native' @@ -407,6 +432,11 @@ export class TitlebarPart extends Part implements ITitleService { return color.isOpaque() ? color : color.makeOpaque(WORKBENCH_BACKGROUND(theme)); }) || ''; this.element.style.backgroundColor = titleBackground; + + if (this.appIconBadge) { + this.appIconBadge.style.backgroundColor = titleBackground; + } + if (titleBackground && Color.fromHex(titleBackground).isLighter()) { this.element.classList.add('light'); } else { @@ -441,7 +471,7 @@ export class TitlebarPart extends Part implements ITitleService { protected adjustTitleMarginToCenter(): void { if (this.customMenubar && this.menubar) { - const leftMarker = this.menubar.clientWidth + 10; + const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; const rightMarker = this.element.clientWidth - 10; // Not enough space to center the titlebar within window, @@ -497,7 +527,7 @@ export class TitlebarPart extends Part implements ITitleService { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { const titlebarActiveFg = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (titlebarActiveFg) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 1d0c40a8b0..aff387c4e8 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -68,6 +68,11 @@ margin-right: auto; } +.monaco-workbench .pane > .pane-body.wide > .welcome-view .monaco-button { + margin-left: inherit; + max-width: 260px; +} + .monaco-workbench .pane > .pane-body .welcome-view-content { padding: 0 20px 0 20px; box-sizing: border-box; @@ -162,6 +167,7 @@ .customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label.codicon { line-height: 22px; + height: 22px; } .customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label.codicon::before { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 05e1f09354..24186cde4f 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -8,19 +8,19 @@ import { toDisposable, IDisposable, Disposable, DisposableStore } from 'vs/base/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { MenuId, IMenuService, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Event, Emitter } from 'vs/base/common/event'; import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; -import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInContextMenuActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -52,6 +52,8 @@ import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Codicon } from 'vs/base/common/codicons'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Command } from 'vs/editor/common/modes'; export class TreeViewPane extends ViewPane { @@ -453,15 +455,7 @@ export class TreeView extends Disposable implements ITreeView { } private createTree() { - const actionViewItemProvider = (action: IAction) => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; - }; + const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); @@ -534,12 +528,13 @@ export class TreeView extends Disposable implements ITreeView { })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); - this._register(this.tree.onDidOpen(e => { + this._register(this.tree.onDidOpen(async (e) => { if (!e.browserEvent) { return; } const selection = this.tree!.getSelection(); - const command = selection.length === 1 ? selection[0].command : undefined; + const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined); + if (command) { let args = command.arguments || []; if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { @@ -554,6 +549,17 @@ export class TreeView extends Disposable implements ITreeView { } + private async resolveCommand(element: ITreeItem | undefined): Promise { + let command = element?.command; + if (element && !command) { + if ((element instanceof ResolvableTreeItem) && element.hasResolve) { + await element.resolve(new CancellationTokenSource().token); + command = element.command; + } + } + return command; + } + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { this.hoverService.hideHover(); const node: ITreeItem | null = treeEvent.element; @@ -776,10 +782,17 @@ class TreeDataSource implements IAsyncDataSource { } async getChildren(element: ITreeItem): Promise { + let result: ITreeItem[] = []; if (this.treeView.dataProvider) { - return this.withProgress(this.treeView.dataProvider.getChildren(element)); + try { + result = await this.withProgress(this.treeView.dataProvider.getChildren(element)); + } catch (e) { + if (!(e.message).startsWith('Bad progress location:')) { + throw e; + } + } } - return []; + return result; } } @@ -868,7 +881,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer(async (resolve) => { - await node.resolve(); - resolve(node.tooltip); - }), - markdownNotSupportedFallback: resource ? undefined : '' // Passing undefined as the fallback for a resource falls back to the old native hover + markdown: (token: CancellationToken): Promise => { + return new Promise(async (resolve) => { + await node.resolve(token); + resolve(node.tooltip); + }); + }, + markdownNotSupportedFallback: resource ? undefined : label ?? '' // Passing undefined as the fallback for a resource falls back to the old native hover }; } @@ -926,7 +941,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer()); - readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; - - constructor( - viewId: string, - menuId: MenuId, - contextMenuId: MenuId, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - ) { - super(); - - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('view', viewId); - - const menu = this._register(this.menuService.createMenu(menuId, scopedContextKeyService)); - const updateActions = () => { - this.primaryActions = []; - this.secondaryActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, { primary: this.primaryActions, secondary: this.secondaryActions }); - this._onDidChangeTitle.fire(); - }; - this._register(menu.onDidChange(updateActions)); - updateActions(); - - const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); - const updateContextMenuActions = () => { - this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); - }; - this._register(contextMenu.onDidChange(updateContextMenuActions)); - updateContextMenuActions(); - - this._register(toDisposable(() => { - this.primaryActions = []; - this.secondaryActions = []; - this.contextMenuActions = []; - })); - } - - getPrimaryActions(): IAction[] { - return this.primaryActions; - } - - getSecondaryActions(): IAction[] { - return this.secondaryActions; - } - - getContextMenuActions(): IAction[] { - return this.contextMenuActions; - } -} - -export class ViewContainerMenuActions extends Disposable { - - private readonly titleActionsDisposable = this._register(new MutableDisposable()); - private contextMenuActions: IAction[] = []; - - constructor( - containerId: string, - contextMenuId: MenuId, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - ) { - super(); - - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('container', containerId); - - const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); - const updateContextMenuActions = () => { - this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); - }; - this._register(contextMenu.onDidChange(updateContextMenuActions)); - updateContextMenuActions(); - - this._register(toDisposable(() => { - this.contextMenuActions = []; - })); - } - - getContextMenuActions(): IAction[] { - return this.contextMenuActions; - } -} diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts new file mode 100644 index 0000000000..82900eda0c --- /dev/null +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -0,0 +1,642 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/paneviewlet'; +import * as nls from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { foreground } from 'vs/platform/theme/common/colorRegistry'; +import { attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptorService, ViewContainerLocation, IViewsRegistry, IViewContentDescriptor, defaultViewIcon, IViewsService, ViewContainerLocationToString } from 'vs/workbench/common/views'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { assertIsDefined } from 'vs/base/common/types'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, Action2, IAction2Options, IMenuService } from 'vs/platform/actions/common/actions'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { parseLinkedText } from 'vs/base/common/linkedText'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Link } from 'vs/platform/opener/browser/link'; +import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; +import { IProgressIndicator } from 'vs/platform/progress/common/progress'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { URI } from 'vs/base/common/uri'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; + +export interface IViewPaneOptions extends IPaneOptions { + id: string; + showActionsAlways?: boolean; + titleMenuId?: MenuId; +} + +type WelcomeActionClassification = { + viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.')); +const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.')); + +const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +interface IItem { + readonly descriptor: IViewContentDescriptor; + visible: boolean; +} + +class ViewWelcomeController { + + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private defaultItem: IItem | undefined; + private items: IItem[] = []; + get contents(): IViewContentDescriptor[] { + const visibleItems = this.items.filter(v => v.visible); + + if (visibleItems.length === 0 && this.defaultItem) { + return [this.defaultItem.descriptor]; + } + + return visibleItems.map(v => v.descriptor); + } + + private contextKeyService: IContextKeyService; + private disposables = new DisposableStore(); + + constructor( + private id: string, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + this.contextKeyService = contextKeyService.createScoped(); + this.disposables.add(this.contextKeyService); + + contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); + Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); + this.onDidChangeViewWelcomeContent(); + } + + private onDidChangeViewWelcomeContent(): void { + const descriptors = viewsRegistry.getViewWelcomeContent(this.id); + + this.items = []; + + for (const descriptor of descriptors) { + if (descriptor.when === 'default') { + this.defaultItem = { descriptor, visible: true }; + } else { + const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; + this.items.push({ descriptor, visible }); + } + } + + this._onDidChange.fire(); + } + + private onDidChangeContext(): void { + let didChange = false; + + for (const item of this.items) { + if (!item.descriptor.when || item.descriptor.when === 'default') { + continue; + } + + const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); + + if (item.visible === visible) { + continue; + } + + item.visible = visible; + didChange = true; + } + + if (didChange) { + this._onDidChange.fire(); + } + } + + dispose(): void { + this.disposables.dispose(); + } +} + +class ViewMenuActions extends CompositeMenuActions { + constructor( + viewId: string, + menuId: MenuId, + contextMenuId: MenuId, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + ) { + const scopedContextKeyService = contextKeyService.createScoped(); + scopedContextKeyService.createKey('view', viewId); + const viewLocationKey = scopedContextKeyService.createKey('viewLocation', ViewContainerLocationToString(viewDescriptorService.getViewLocationById(viewId)!)); + super(menuId, contextMenuId, { shouldForwardArgs: true }, scopedContextKeyService, menuService); + this._register(scopedContextKeyService); + this._register(Event.filter(viewDescriptorService.onDidChangeLocation, e => e.views.some(view => view.id === viewId))(() => viewLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewLocationById(viewId)!)))); + } + +} + +export abstract class ViewPane extends Pane implements IView { + + private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; + + private _onDidFocus = this._register(new Emitter()); + readonly onDidFocus: Event = this._onDidFocus.event; + + private _onDidBlur = this._register(new Emitter()); + readonly onDidBlur: Event = this._onDidBlur.event; + + private _onDidChangeBodyVisibility = this._register(new Emitter()); + readonly onDidChangeBodyVisibility: Event = this._onDidChangeBodyVisibility.event; + + protected _onDidChangeTitleArea = this._register(new Emitter()); + readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; + + protected _onDidChangeViewWelcomeState = this._register(new Emitter()); + readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; + + private focusedViewContextKey: IContextKey; + + private _isVisible: boolean = false; + readonly id: string; + + private _title: string; + public get title(): string { + return this._title; + } + + private _titleDescription: string | undefined; + public get titleDescription(): string | undefined { + return this._titleDescription; + } + + private readonly menuActions: ViewMenuActions; + private progressBar!: ProgressBar; + private progressIndicator!: IProgressIndicator; + + private toolbar?: ToolBar; + private readonly showActionsAlways: boolean = false; + private headerContainer?: HTMLElement; + private titleContainer?: HTMLElement; + private titleDescriptionContainer?: HTMLElement; + private iconContainer?: HTMLElement; + protected twistiesContainer?: HTMLElement; + + private bodyContainer!: HTMLElement; + private viewWelcomeContainer!: HTMLElement; + private viewWelcomeDisposable: IDisposable = Disposable.None; + private viewWelcomeController: ViewWelcomeController; + + constructor( + options: IViewPaneOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IOpenerService protected openerService: IOpenerService, + @IThemeService protected themeService: IThemeService, + @ITelemetryService protected telemetryService: ITelemetryService, + ) { + super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); + + this.id = options.id; + this._title = options.title; + this._titleDescription = options.titleDescription; + this.showActionsAlways = !!options.showActionsAlways; + this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); + + this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); + this._register(this.menuActions.onDidChange(() => this.updateActions())); + + this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); + } + + get headerVisible(): boolean { + return super.headerVisible; + } + + set headerVisible(visible: boolean) { + super.headerVisible = visible; + this.element.classList.toggle('merged-header', !visible); + } + + setVisible(visible: boolean): void { + if (this._isVisible !== visible) { + this._isVisible = visible; + + if (this.isExpanded()) { + this._onDidChangeBodyVisibility.fire(visible); + } + } + } + + isVisible(): boolean { + return this._isVisible; + } + + isBodyVisible(): boolean { + return this._isVisible && this.isExpanded(); + } + + setExpanded(expanded: boolean): boolean { + const changed = super.setExpanded(expanded); + if (changed) { + this._onDidChangeBodyVisibility.fire(expanded); + } + if (this.twistiesContainer) { + this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!expanded))); + this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(expanded))); + } + return changed; + } + + render(): void { + super.render(); + + const focusTracker = trackFocus(this.element); + this._register(focusTracker); + this._register(focusTracker.onDidFocus(() => { + this.focusedViewContextKey.set(this.id); + this._onDidFocus.fire(); + })); + this._register(focusTracker.onDidBlur(() => { + if (this.focusedViewContextKey.get() === this.id) { + this.focusedViewContextKey.reset(); + } + + this._onDidBlur.fire(); + })); + } + + protected renderHeader(container: HTMLElement): void { + this.headerContainer = container; + + this.twistiesContainer = append(container, $(ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded())))); + + this.renderHeaderTitle(container, this.title); + + const actions = append(container, $('.actions')); + actions.classList.toggle('show', this.showActionsAlways); + this.toolbar = new ToolBar(actions, this.contextMenuService, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: action => this.getActionViewItem(action), + ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + renderDropdownAsChildElement: true + }); + + this._register(this.toolbar); + this.setActions(); + + this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); + + this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { + this.updateTitle(this.title); + })); + + const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); + this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); + this.updateActionsVisibility(); + } + + protected getTwistyIcon(expanded: boolean): ThemeIcon { + return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon; + } + + style(styles: IPaneStyles): void { + super.style(styles); + + const icon = this.getIcon(); + if (this.iconContainer) { + const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); + if (URI.isUri(icon)) { + // Apply background color to activity bar item provided with iconUrls + this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.color = ''; + } else { + // Apply foreground color to activity bar items provided with codicons + this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.backgroundColor = ''; + } + } + } + + private getIcon(): ThemeIcon | URI { + return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon; + } + + protected renderHeaderTitle(container: HTMLElement, title: string): void { + this.iconContainer = append(container, $('.icon', undefined)); + const icon = this.getIcon(); + + let cssClass: string | undefined = undefined; + if (URI.isUri(icon)) { + cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; + const iconClass = `.pane-header .icon.${cssClass}`; + + createCSSRule(iconClass, ` + mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask-size: 24px; + -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask-size: 16px; + `); + } else if (ThemeIcon.isThemeIcon(icon)) { + cssClass = ThemeIcon.asClassName(icon); + } + + if (cssClass) { + this.iconContainer.classList.add(...cssClass.split(' ')); + } + + const calculatedTitle = this.calculateTitle(title); + this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); + + if (this._titleDescription) { + this.setTitleDescription(this._titleDescription); + } + + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + protected updateTitle(title: string): void { + const calculatedTitle = this.calculateTitle(title); + if (this.titleContainer) { + this.titleContainer.textContent = calculatedTitle; + this.titleContainer.setAttribute('title', calculatedTitle); + } + + if (this.iconContainer) { + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + + this._title = title; + this._onDidChangeTitleArea.fire(); + } + + private setTitleDescription(description: string | undefined) { + if (this.titleDescriptionContainer) { + this.titleDescriptionContainer.textContent = description ?? ''; + this.titleDescriptionContainer.setAttribute('title', description ?? ''); + } + else if (description && this.titleContainer) { + this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); + } + } + + protected updateTitleDescription(description?: string | undefined): void { + this.setTitleDescription(description); + + this._titleDescription = description; + this._onDidChangeTitleArea.fire(); + } + + private calculateTitle(title: string): string { + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; + const model = this.viewDescriptorService.getViewContainerModel(viewContainer); + const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); + const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; + + if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { + return `${viewDescriptor.containerTitle}: ${title}`; + } + + return title; + } + + private scrollableElement!: DomScrollableElement; + + protected renderBody(container: HTMLElement): void { + this.bodyContainer = container; + + const viewWelcomeContainer = append(container, $('.welcome-view')); + this.viewWelcomeContainer = $('.welcome-view-content', { tabIndex: 0 }); + this.scrollableElement = this._register(new DomScrollableElement(this.viewWelcomeContainer, { + alwaysConsumeMouseWheel: true, + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Visible, + })); + + append(viewWelcomeContainer, this.scrollableElement.getDomNode()); + + const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); + this._register(onViewWelcomeChange(this.updateViewWelcome, this)); + this.updateViewWelcome(); + } + + protected layoutBody(height: number, width: number): void { + this.viewWelcomeContainer.style.height = `${height}px`; + this.viewWelcomeContainer.style.width = `${width}px`; + this.scrollableElement.scanDomNode(); + } + + getProgressIndicator() { + if (this.progressBar === undefined) { + // Progress bar + this.progressBar = this._register(new ProgressBar(this.element)); + this._register(attachProgressBarStyler(this.progressBar, this.themeService)); + this.progressBar.hide(); + } + + if (this.progressIndicator === undefined) { + this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); + } + return this.progressIndicator; + } + + protected getProgressLocation(): string { + return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; + } + + protected getBackgroundColor(): string { + return this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND; + } + + focus(): void { + if (this.shouldShowWelcome()) { + this.viewWelcomeContainer.focus(); + } else if (this.element) { + this.element.focus(); + this._onDidFocus.fire(); + } + } + + private setActions(): void { + if (this.toolbar) { + this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); + this.toolbar.context = this.getActionsContext(); + } + } + + private updateActionsVisibility(): void { + if (!this.headerContainer) { + return; + } + const shouldAlwaysShowActions = this.configurationService.getValue('workbench.view.alwaysShowHeaderActions'); + this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions); + } + + protected updateActions(): void { + this.setActions(); + this._onDidChangeTitleArea.fire(); + } + + getActions(): IAction[] { + return this.menuActions.getPrimaryActions(); + } + + getSecondaryActions(): IAction[] { + return this.menuActions.getSecondaryActions(); + } + + getContextMenuActions(): IAction[] { + return this.menuActions.getContextMenuActions(); + } + + getActionViewItem(action: IAction): IActionViewItem | undefined { + return createActionViewItem(this.instantiationService, action); + } + + getActionsContext(): unknown { + return undefined; + } + + getOptimalWidth(): number { + return 0; + } + + saveState(): void { + // Subclasses to implement for saving state + } + + private updateViewWelcome(): void { + this.viewWelcomeDisposable.dispose(); + + if (!this.shouldShowWelcome()) { + this.bodyContainer.classList.remove('welcome'); + this.viewWelcomeContainer.innerText = ''; + this.scrollableElement.scanDomNode(); + return; + } + + const contents = this.viewWelcomeController.contents; + + if (contents.length === 0) { + this.bodyContainer.classList.remove('welcome'); + this.viewWelcomeContainer.innerText = ''; + this.scrollableElement.scanDomNode(); + return; + } + + const disposables = new DisposableStore(); + this.bodyContainer.classList.add('welcome'); + this.viewWelcomeContainer.innerText = ''; + + for (const { content, precondition } of contents) { + const lines = content.split('\n'); + + for (let line of lines) { + line = line.trim(); + + if (!line) { + continue; + } + + const linkedText = parseLinkedText(line); + + if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { + const node = linkedText.nodes[0]; + const button = new Button(this.viewWelcomeContainer, { title: node.title, supportIcons: true }); + button.label = node.label; + button.onDidClick(_ => { + this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); + this.openerService.open(node.href); + }, null, disposables); + disposables.add(button); + disposables.add(attachButtonStyler(button, this.themeService)); + + if (precondition) { + const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } else { + const p = append(this.viewWelcomeContainer, $('p')); + + for (const node of linkedText.nodes) { + if (typeof node === 'string') { + append(p, document.createTextNode(node)); + } else { + const link = this.instantiationService.createInstance(Link, node); + append(p, link.el); + disposables.add(link); + disposables.add(attachLinkStyler(link, this.themeService)); + + if (precondition && node.href.startsWith('command:')) { + const updateEnablement = () => link.style({ disabled: !this.contextKeyService.contextMatchesRules(precondition) }); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } + } + } + } + } + + this.scrollableElement.scanDomNode(); + this.viewWelcomeDisposable = disposables; + } + + shouldShowWelcome(): boolean { + return false; + } +} + +export abstract class ViewAction extends Action2 { + constructor(readonly desc: Readonly & { viewId: string }) { + super(desc); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const view = accessor.get(IViewsService).getActiveViewWithId(this.desc.viewId); + if (view) { + return this.runInView(accessor, view, ...args); + } + } + + abstract runInView(accessor: ServicesAccessor, view: T, ...args: any[]): any; +} diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 521ef2e204..6006162ecf 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -6,52 +6,45 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; -import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, isAncestor, Dimension, addDisposableListener, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; -import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; -import { ActionsOrientation, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ColorIdentifier, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; +import { EventType, Dimension, addDisposableListener, isAncestor } from 'vs/base/browser/dom'; +import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { PaneView, IPaneViewOptions, IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { PaneView, IPaneViewOptions } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel, defaultViewIcon } from 'vs/workbench/common/views'; +import { IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel, IViewsService, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Component } from 'vs/workbench/common/component'; -import { MenuId, MenuItemAction, registerAction2, Action2, IAction2Options, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; -import { parseLinkedText } from 'vs/base/common/linkedText'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { Link } from 'vs/platform/opener/browser/link'; +import { registerAction2, Action2, IAction2Options, IMenuService, MenuId, MenuRegistry, ISubmenuItem, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { CompositeDragAndDropObserver, DragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; -import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { URI } from 'vs/base/common/uri'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { Codicon } from 'vs/base/common/codicons'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { CompositeMenuActions } from 'vs/workbench/browser/menuActions'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export const ViewsSubMenu = new MenuId('Views'); +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + submenu: ViewsSubMenu, + title: nls.localize('views', "Views"), + order: 1, + when: ContextKeyEqualsExpr.create('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar)), +}); export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -61,566 +54,6 @@ export interface IPaneColors extends IColorMapping { leftBorder?: ColorIdentifier; } -export interface IViewPaneOptions extends IPaneOptions { - id: string; - showActionsAlways?: boolean; - titleMenuId?: MenuId; -} - -type WelcomeActionClassification = { - viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -const viewPaneContainerExpandedIcon = registerIcon('view-pane-container-expanded', Codicon.chevronDown, nls.localize('viewPaneContainerExpandedIcon', 'Icon for an expanded view pane container.')); -const viewPaneContainerCollapsedIcon = registerIcon('view-pane-container-collapsed', Codicon.chevronRight, nls.localize('viewPaneContainerCollapsedIcon', 'Icon for a collapsed view pane container.')); - -const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); - -interface IItem { - readonly descriptor: IViewContentDescriptor; - visible: boolean; -} - -class ViewWelcomeController { - - private _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - private defaultItem: IItem | undefined; - private items: IItem[] = []; - get contents(): IViewContentDescriptor[] { - const visibleItems = this.items.filter(v => v.visible); - - if (visibleItems.length === 0 && this.defaultItem) { - return [this.defaultItem.descriptor]; - } - - return visibleItems.map(v => v.descriptor); - } - - private contextKeyService: IContextKeyService; - private disposables = new DisposableStore(); - - constructor( - private id: string, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - this.contextKeyService = contextKeyService.createScoped(); - this.disposables.add(this.contextKeyService); - - contextKeyService.onDidChangeContext(this.onDidChangeContext, this, this.disposables); - Event.filter(viewsRegistry.onDidChangeViewWelcomeContent, id => id === this.id)(this.onDidChangeViewWelcomeContent, this, this.disposables); - this.onDidChangeViewWelcomeContent(); - } - - private onDidChangeViewWelcomeContent(): void { - const descriptors = viewsRegistry.getViewWelcomeContent(this.id); - - this.items = []; - - for (const descriptor of descriptors) { - if (descriptor.when === 'default') { - this.defaultItem = { descriptor, visible: true }; - } else { - const visible = descriptor.when ? this.contextKeyService.contextMatchesRules(descriptor.when) : true; - this.items.push({ descriptor, visible }); - } - } - - this._onDidChange.fire(); - } - - private onDidChangeContext(): void { - let didChange = false; - - for (const item of this.items) { - if (!item.descriptor.when || item.descriptor.when === 'default') { - continue; - } - - const visible = this.contextKeyService.contextMatchesRules(item.descriptor.when); - - if (item.visible === visible) { - continue; - } - - item.visible = visible; - didChange = true; - } - - if (didChange) { - this._onDidChange.fire(); - } - } - - dispose(): void { - this.disposables.dispose(); - } -} - -export abstract class ViewPane extends Pane implements IView { - - private static readonly AlwaysShowActionsConfig = 'workbench.view.alwaysShowHeaderActions'; - - private _onDidFocus = this._register(new Emitter()); - readonly onDidFocus: Event = this._onDidFocus.event; - - private _onDidBlur = this._register(new Emitter()); - readonly onDidBlur: Event = this._onDidBlur.event; - - private _onDidChangeBodyVisibility = this._register(new Emitter()); - readonly onDidChangeBodyVisibility: Event = this._onDidChangeBodyVisibility.event; - - protected _onDidChangeTitleArea = this._register(new Emitter()); - readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; - - protected _onDidChangeViewWelcomeState = this._register(new Emitter()); - readonly onDidChangeViewWelcomeState: Event = this._onDidChangeViewWelcomeState.event; - - private focusedViewContextKey: IContextKey; - - private _isVisible: boolean = false; - readonly id: string; - - private _title: string; - public get title(): string { - return this._title; - } - - private _titleDescription: string | undefined; - public get titleDescription(): string | undefined { - return this._titleDescription; - } - - private readonly menuActions: ViewMenuActions; - private progressBar!: ProgressBar; - private progressIndicator!: IProgressIndicator; - - private toolbar?: ToolBar; - private readonly showActionsAlways: boolean = false; - private headerContainer?: HTMLElement; - private titleContainer?: HTMLElement; - private titleDescriptionContainer?: HTMLElement; - private iconContainer?: HTMLElement; - protected twistiesContainer?: HTMLElement; - - private bodyContainer!: HTMLElement; - private viewWelcomeContainer!: HTMLElement; - private viewWelcomeDisposable: IDisposable = Disposable.None; - private viewWelcomeController: ViewWelcomeController; - - constructor( - options: IViewPaneOptions, - @IKeybindingService protected keybindingService: IKeybindingService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IConfigurationService protected readonly configurationService: IConfigurationService, - @IContextKeyService protected contextKeyService: IContextKeyService, - @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IOpenerService protected openerService: IOpenerService, - @IThemeService protected themeService: IThemeService, - @ITelemetryService protected telemetryService: ITelemetryService, - ) { - super({ ...options, ...{ orientation: viewDescriptorService.getViewLocationById(options.id) === ViewContainerLocation.Panel ? Orientation.HORIZONTAL : Orientation.VERTICAL } }); - - this.id = options.id; - this._title = options.title; - this._titleDescription = options.titleDescription; - this.showActionsAlways = !!options.showActionsAlways; - this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); - - this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId || MenuId.ViewTitle, MenuId.ViewTitleContext)); - this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); - - this.viewWelcomeController = new ViewWelcomeController(this.id, contextKeyService); - } - - get headerVisible(): boolean { - return super.headerVisible; - } - - set headerVisible(visible: boolean) { - super.headerVisible = visible; - this.element.classList.toggle('merged-header', !visible); - } - - setVisible(visible: boolean): void { - if (this._isVisible !== visible) { - this._isVisible = visible; - - if (this.isExpanded()) { - this._onDidChangeBodyVisibility.fire(visible); - } - } - } - - isVisible(): boolean { - return this._isVisible; - } - - isBodyVisible(): boolean { - return this._isVisible && this.isExpanded(); - } - - setExpanded(expanded: boolean): boolean { - const changed = super.setExpanded(expanded); - if (changed) { - this._onDidChangeBodyVisibility.fire(expanded); - } - if (this.twistiesContainer) { - this.twistiesContainer.classList.remove(...ThemeIcon.asClassNameArray(this.getTwistyIcon(!expanded))); - this.twistiesContainer.classList.add(...ThemeIcon.asClassNameArray(this.getTwistyIcon(expanded))); - } - return changed; - } - - render(): void { - super.render(); - - const focusTracker = trackFocus(this.element); - this._register(focusTracker); - this._register(focusTracker.onDidFocus(() => { - this.focusedViewContextKey.set(this.id); - this._onDidFocus.fire(); - })); - this._register(focusTracker.onDidBlur(() => { - if (this.focusedViewContextKey.get() === this.id) { - this.focusedViewContextKey.reset(); - } - - this._onDidBlur.fire(); - })); - } - - protected renderHeader(container: HTMLElement): void { - this.headerContainer = container; - - this.twistiesContainer = append(container, $(ThemeIcon.asCSSSelector(this.getTwistyIcon(this.isExpanded())))); - - this.renderHeaderTitle(container, this.title); - - const actions = append(container, $('.actions')); - actions.classList.toggle('show', this.showActionsAlways); - this.toolbar = new ToolBar(actions, this.contextMenuService, { - orientation: ActionsOrientation.HORIZONTAL, - actionViewItemProvider: action => this.getActionViewItem(action), - ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - renderDropdownAsChildElement: true - }); - - this._register(this.toolbar); - this.setActions(); - - this._register(addDisposableListener(actions, EventType.CLICK, e => e.preventDefault())); - - this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { - this.updateTitle(this.title); - })); - - const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); - this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); - this.updateActionsVisibility(); - } - - protected getTwistyIcon(expanded: boolean): ThemeIcon { - return expanded ? viewPaneContainerExpandedIcon : viewPaneContainerCollapsedIcon; - } - - style(styles: IPaneStyles): void { - super.style(styles); - - const icon = this.getIcon(); - if (this.iconContainer) { - const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); - if (URI.isUri(icon)) { - // Apply background color to activity bar item provided with iconUrls - this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; - this.iconContainer.style.color = ''; - } else { - // Apply foreground color to activity bar items provided with codicons - this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; - this.iconContainer.style.backgroundColor = ''; - } - } - } - - private getIcon(): ThemeIcon | URI { - return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || defaultViewIcon; - } - - protected renderHeaderTitle(container: HTMLElement, title: string): void { - this.iconContainer = append(container, $('.icon', undefined)); - const icon = this.getIcon(); - - let cssClass: string | undefined = undefined; - if (URI.isUri(icon)) { - cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; - const iconClass = `.pane-header .icon.${cssClass}`; - - createCSSRule(iconClass, ` - mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - mask-size: 24px; - -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; - -webkit-mask-size: 16px; - `); - } else if (ThemeIcon.isThemeIcon(icon)) { - cssClass = ThemeIcon.asClassName(icon); - } - - if (cssClass) { - this.iconContainer.classList.add(...cssClass.split(' ')); - } - - const calculatedTitle = this.calculateTitle(title); - this.titleContainer = append(container, $('h3.title', { title: calculatedTitle }, calculatedTitle)); - - if (this._titleDescription) { - this.setTitleDescription(this._titleDescription); - } - - this.iconContainer.title = calculatedTitle; - this.iconContainer.setAttribute('aria-label', calculatedTitle); - } - - protected updateTitle(title: string): void { - const calculatedTitle = this.calculateTitle(title); - if (this.titleContainer) { - this.titleContainer.textContent = calculatedTitle; - this.titleContainer.setAttribute('title', calculatedTitle); - } - - if (this.iconContainer) { - this.iconContainer.title = calculatedTitle; - this.iconContainer.setAttribute('aria-label', calculatedTitle); - } - - this._title = title; - this._onDidChangeTitleArea.fire(); - } - - private setTitleDescription(description: string | undefined) { - if (this.titleDescriptionContainer) { - this.titleDescriptionContainer.textContent = description ?? ''; - this.titleDescriptionContainer.setAttribute('title', description ?? ''); - } - else if (description && this.titleContainer) { - this.titleDescriptionContainer = after(this.titleContainer, $('span.description', { title: description }, description)); - } - } - - protected updateTitleDescription(description?: string | undefined): void { - this.setTitleDescription(description); - - this._titleDescription = description; - this._onDidChangeTitleArea.fire(); - } - - private calculateTitle(title: string): string { - const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; - const model = this.viewDescriptorService.getViewContainerModel(viewContainer); - const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); - const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; - - if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { - return `${viewDescriptor.containerTitle}: ${title}`; - } - - return title; - } - - private scrollableElement!: DomScrollableElement; - - protected renderBody(container: HTMLElement): void { - this.bodyContainer = container; - - const viewWelcomeContainer = append(container, $('.welcome-view')); - this.viewWelcomeContainer = $('.welcome-view-content', { tabIndex: 0 }); - this.scrollableElement = this._register(new DomScrollableElement(this.viewWelcomeContainer, { - alwaysConsumeMouseWheel: true, - horizontal: ScrollbarVisibility.Hidden, - vertical: ScrollbarVisibility.Visible, - })); - - append(viewWelcomeContainer, this.scrollableElement.getDomNode()); - - const onViewWelcomeChange = Event.any(this.viewWelcomeController.onDidChange, this.onDidChangeViewWelcomeState); - this._register(onViewWelcomeChange(this.updateViewWelcome, this)); - this.updateViewWelcome(); - } - - protected layoutBody(height: number, width: number): void { - this.viewWelcomeContainer.style.height = `${height}px`; - this.viewWelcomeContainer.style.width = `${width}px`; - this.scrollableElement.scanDomNode(); - } - - getProgressIndicator() { - if (this.progressBar === undefined) { - // Progress bar - this.progressBar = this._register(new ProgressBar(this.element)); - this._register(attachProgressBarStyler(this.progressBar, this.themeService)); - this.progressBar.hide(); - } - - if (this.progressIndicator === undefined) { - this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); - } - return this.progressIndicator; - } - - protected getProgressLocation(): string { - return this.viewDescriptorService.getViewContainerByViewId(this.id)!.id; - } - - protected getBackgroundColor(): string { - return this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND; - } - - focus(): void { - if (this.shouldShowWelcome()) { - this.viewWelcomeContainer.focus(); - } else if (this.element) { - this.element.focus(); - this._onDidFocus.fire(); - } - } - - private setActions(): void { - if (this.toolbar) { - this.toolbar.setActions(prepareActions(this.getActions()), prepareActions(this.getSecondaryActions())); - this.toolbar.context = this.getActionsContext(); - } - } - - private updateActionsVisibility(): void { - if (!this.headerContainer) { - return; - } - const shouldAlwaysShowActions = this.configurationService.getValue('workbench.view.alwaysShowHeaderActions'); - this.headerContainer.classList.toggle('actions-always-visible', shouldAlwaysShowActions); - } - - protected updateActions(): void { - this.setActions(); - this._onDidChangeTitleArea.fire(); - } - - getActions(): IAction[] { - return this.menuActions.getPrimaryActions(); - } - - getSecondaryActions(): IAction[] { - return this.menuActions.getSecondaryActions(); - } - - getContextMenuActions(): IAction[] { - return this.menuActions.getContextMenuActions(); - } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - return undefined; - } - - getActionsContext(): unknown { - return undefined; - } - - getOptimalWidth(): number { - return 0; - } - - saveState(): void { - // Subclasses to implement for saving state - } - - private updateViewWelcome(): void { - this.viewWelcomeDisposable.dispose(); - - if (!this.shouldShowWelcome()) { - this.bodyContainer.classList.remove('welcome'); - this.viewWelcomeContainer.innerText = ''; - this.scrollableElement.scanDomNode(); - return; - } - - const contents = this.viewWelcomeController.contents; - - if (contents.length === 0) { - this.bodyContainer.classList.remove('welcome'); - this.viewWelcomeContainer.innerText = ''; - this.scrollableElement.scanDomNode(); - return; - } - - const disposables = new DisposableStore(); - this.bodyContainer.classList.add('welcome'); - this.viewWelcomeContainer.innerText = ''; - - for (const { content, precondition } of contents) { - const lines = content.split('\n'); - - for (let line of lines) { - line = line.trim(); - - if (!line) { - continue; - } - - const linkedText = parseLinkedText(line); - - if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { - const node = linkedText.nodes[0]; - const button = new Button(this.viewWelcomeContainer, { title: node.title, supportCodicons: true }); - button.label = node.label; - button.onDidClick(_ => { - this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); - this.openerService.open(node.href, { allowCommands: true }); - }, null, disposables); - disposables.add(button); - disposables.add(attachButtonStyler(button, this.themeService)); - - if (precondition) { - const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); - updateEnablement(); - - const keys = new Set(); - precondition.keys().forEach(key => keys.add(key)); - const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); - onDidChangeContext(updateEnablement, null, disposables); - } - } else { - const p = append(this.viewWelcomeContainer, $('p')); - - for (const node of linkedText.nodes) { - if (typeof node === 'string') { - append(p, document.createTextNode(node)); - } else { - const link = this.instantiationService.createInstance(Link, node); - append(p, link.el); - disposables.add(link); - disposables.add(attachLinkStyler(link, this.themeService)); - } - } - } - } - } - - this.scrollableElement.scanDomNode(); - this.viewWelcomeDisposable = disposables; - } - - shouldShowWelcome(): boolean { - return false; - } -} - export interface IViewPaneContainerOptions extends IPaneViewOptions { mergeViewWithContainerWhenSingleView: boolean; } @@ -860,6 +293,22 @@ class ViewPaneDropOverlay extends Themable { } } +class ViewContainerMenuActions extends CompositeMenuActions { + constructor( + viewContainer: ViewContainer, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + ) { + const scopedContextKeyService = contextKeyService.createScoped(); + scopedContextKeyService.createKey('viewContainer', viewContainer.id); + const viewContainerLocationKey = scopedContextKeyService.createKey('viewContainerLocation', ViewContainerLocationToString(viewDescriptorService.getViewContainerLocation(viewContainer)!)); + super(MenuId.ViewContainerTitle, MenuId.ViewContainerTitleContext, { shouldForwardArgs: true }, scopedContextKeyService, menuService); + this._register(scopedContextKeyService); + this._register(Event.filter(viewDescriptorService.onDidChangeContainerLocation, e => e.viewContainer === viewContainer)(() => viewContainerLocationKey.set(ViewContainerLocationToString(viewDescriptorService.getViewContainerLocation(viewContainer)!)))); + } +} + export class ViewPaneContainer extends Component implements IViewPaneContainer { readonly viewContainer: ViewContainer; @@ -910,6 +359,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return this.paneItems.length; } + private readonly menuActions: ViewContainerMenuActions; + constructor( id: string, private options: IViewPaneContainerOptions, @@ -922,7 +373,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { @IThemeService protected themeService: IThemeService, @IStorageService protected storageService: IStorageService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService + @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, ) { super(id, themeService, storageService); @@ -938,6 +389,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined); this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); this.viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + + this.menuActions = this._register(instantiationService.createInstance(ViewContainerMenuActions, container)); + this._register(this.menuActions.onDidChange(() => this.updateTitleArea())); } create(parent: HTMLElement): void { @@ -1110,57 +564,62 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { let anchor: { x: number, y: number; } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => this.getContextMenuActions() + getActions: () => [...this.getContextMenuActions2()] }); } + getContextMenuActions2(): ReadonlyArray { + return this.menuActions.getContextMenuActions(); + } + getContextMenuActions(viewDescriptor?: IViewDescriptor): IAction[] { - const result: IAction[] = []; + return []; + } - let showHide = true; - if (!viewDescriptor && this.isViewMergedWithContainer()) { - viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.panes[0].id) || undefined; - showHide = false; + getActions2(): IAction[] { + const result = []; + result.push(...this.menuActions.getPrimaryActions()); + if (this.isViewMergedWithContainer()) { + result.push(...this.paneItems[0].pane.getActions()); } - - if (viewDescriptor) { - if (showHide) { - result.push({ - id: `${viewDescriptor.id}.removeView`, - label: nls.localize('hideView', "Hide"), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor!.id) - }); - } - const view = this.getView(viewDescriptor.id); - if (view) { - result.push(...view.getContextMenuActions()); - } - } - - const viewToggleActions = this.getViewsVisibilityActions(); - if (result.length && viewToggleActions.length) { - result.push(new Separator()); - } - - result.push(...viewToggleActions); - return result; } getActions(): IAction[] { - if (this.isViewMergedWithContainer()) { - return this.paneItems[0].pane.getActions(); - } - return []; } - getSecondaryActions(): IAction[] { - if (this.isViewMergedWithContainer()) { - return this.paneItems[0].pane.getSecondaryActions(); + getSecondaryActions2(): IAction[] { + const viewPaneActions = this.isViewMergedWithContainer() ? this.paneItems[0].pane.getSecondaryActions() : []; + let menuActions = this.menuActions.getSecondaryActions(); + + const viewsSubmenuActionIndex = menuActions.findIndex(action => action instanceof SubmenuItemAction && action.item.submenu === ViewsSubMenu); + if (viewsSubmenuActionIndex !== -1) { + const viewsSubmenuAction = menuActions[viewsSubmenuActionIndex]; + if (viewsSubmenuAction.actions.some(({ enabled }) => enabled)) { + if (menuActions.length === 1 && viewPaneActions.length === 0) { + menuActions = viewsSubmenuAction.actions.slice(); + } else if (viewsSubmenuActionIndex !== 0) { + menuActions = [viewsSubmenuAction, ...menuActions.slice(0, viewsSubmenuActionIndex), ...menuActions.slice(viewsSubmenuActionIndex + 1)]; + } + } else { + // Remove views submenu if none of the actions are enabled + menuActions.splice(viewsSubmenuActionIndex, 1); + } } + if (menuActions.length && viewPaneActions.length) { + return [ + ...menuActions, + new Separator(), + ...viewPaneActions + ]; + } + + return menuActions.length ? menuActions : viewPaneActions; + } + + getSecondaryActions(): IAction[] { return []; } @@ -1168,22 +627,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return undefined; } - getViewsVisibilityActions(): IAction[] { - return this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1), - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - } - getActionViewItem(action: IAction): IActionViewItem | undefined { if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getActionViewItem(action); } - - return undefined; + return createActionViewItem(this.instantiationService, action); } focus(): void { @@ -1321,11 +769,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE, StorageTarget.USER); } - private onContextMenu(event: StandardMouseEvent, viewDescriptor: IViewDescriptor): void { + private onContextMenu(event: StandardMouseEvent, viewPane: ViewPane): void { event.stopPropagation(); event.preventDefault(); - const actions: IAction[] = this.getContextMenuActions(viewDescriptor); + const actions: IAction[] = viewPane.getContextMenuActions(); let anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ @@ -1364,7 +812,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => { e.stopPropagation(); e.preventDefault(); - this.onContextMenu(new StandardMouseEvent(e), viewDescriptor); + this.onContextMenu(new StandardMouseEvent(e), pane); }); const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => { @@ -1401,7 +849,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - protected toggleViewVisibility(viewId: string): void { + toggleViewVisibility(viewId: string): void { // Check if view is active if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) { const visible = !this.viewContainerModel.isVisible(viewId); @@ -1641,7 +1089,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - private isViewMergedWithContainer(): boolean { + isViewMergedWithContainer(): boolean { if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } @@ -1665,6 +1113,21 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } +export abstract class ViewPaneContainerAction extends Action2 { + constructor(readonly desc: Readonly & { viewPaneContainerId: string }) { + super(desc); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(this.desc.viewPaneContainerId); + if (viewPaneContainer) { + return this.runInViewPaneContainer(accessor, viewPaneContainer, ...args); + } + } + + abstract runInViewPaneContainer(accessor: ServicesAccessor, viewPaneContainer: T, ...args: any[]): any; +} + class MoveViewPosition extends Action2 { constructor(desc: Readonly, private readonly offset: number) { super(desc); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index 70afb67494..149eaf36d2 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IViewDescriptorService, ViewContainer, IViewDescriptor, IView, ViewContainerLocation, IViewsService, IViewPaneContainer, getVisbileViewContextKey } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainer, IViewDescriptor, IView, ViewContainerLocation, IViewsService, IViewPaneContainer, getVisbileViewContextKey, getEnabledViewContainerContextKey, FocusedViewContext } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyDefinedExpr, ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { isString } from 'vs/base/common/types'; -import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { MenuId, registerAction2, Action2, MenuRegistry, ICommandActionTitle, ILocalizedString } from 'vs/platform/actions/common/actions'; import { localize } from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -27,10 +27,11 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Viewlet, ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { URI } from 'vs/base/common/uri'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; export class ViewsService extends Disposable implements IViewsService { @@ -139,6 +140,7 @@ export class ViewsService extends Disposable implements IViewsService { this.onViewDescriptorsAdded(added, viewContainer); this.onViewDescriptorsRemoved(removed); })); + this._register(this.registerOpenViewContainerAction(viewContainer)); } private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void { @@ -155,67 +157,9 @@ export class ViewsService extends Disposable implements IViewsService { const composite = this.getComposite(container.id, location); for (const viewDescriptor of views) { const disposables = new DisposableStore(); - disposables.add(registerAction2(class FocusViewAction extends Action2 { - constructor() { - super({ - id: viewDescriptor.focusCommand ? viewDescriptor.focusCommand.id : `${viewDescriptor.id}.focus`, - title: { original: `Focus on ${viewDescriptor.name} View`, value: localize({ key: 'focus view', comment: ['{0} indicates the name of the view to be focused.'] }, "Focus on {0} View", viewDescriptor.name) }, - category: composite ? composite.name : CATEGORIES.View, - menu: [{ - id: MenuId.CommandPalette, - when: viewDescriptor.when, - }], - keybinding: { - when: ContextKeyExpr.has(`${viewDescriptor.id}.active`), - weight: KeybindingWeight.WorkbenchContrib, - primary: viewDescriptor.focusCommand?.keybindings?.primary, - secondary: viewDescriptor.focusCommand?.keybindings?.secondary, - linux: viewDescriptor.focusCommand?.keybindings?.linux, - mac: viewDescriptor.focusCommand?.keybindings?.mac, - win: viewDescriptor.focusCommand?.keybindings?.win - } - }); - } - run(accessor: ServicesAccessor): void { - accessor.get(IViewsService).openView(viewDescriptor.id, true); - } - })); - - disposables.add(registerAction2(class ResetViewLocationAction extends Action2 { - constructor() { - super({ - id: `${viewDescriptor.id}.resetViewLocation`, - title: { - original: 'Reset Location', - value: localize('resetViewLocation', "Reset Location") - }, - menu: [{ - id: MenuId.ViewTitleContext, - when: ContextKeyExpr.or( - ContextKeyExpr.and( - ContextKeyExpr.equals('view', viewDescriptor.id), - ContextKeyExpr.equals(`${viewDescriptor.id}.defaultViewLocation`, false) - ) - ) - }], - }); - } - run(accessor: ServicesAccessor): void { - const viewDescriptorService = accessor.get(IViewDescriptorService); - const defaultContainer = viewDescriptorService.getDefaultContainerById(viewDescriptor.id)!; - const containerModel = viewDescriptorService.getViewContainerModel(defaultContainer)!; - - // The default container is hidden so we should try to reset its location first - if (defaultContainer.hideIfEmpty && containerModel.visibleViewDescriptors.length === 0) { - const defaultLocation = viewDescriptorService.getDefaultViewContainerLocation(defaultContainer)!; - viewDescriptorService.moveViewContainerToLocation(defaultContainer, defaultLocation); - } - - viewDescriptorService.moveViewsToContainer([viewDescriptor], viewDescriptorService.getDefaultContainerById(viewDescriptor.id)!); - accessor.get(IViewsService).openView(viewDescriptor.id, true); - } - })); - + disposables.add(this.registerOpenViewAction(viewDescriptor)); + disposables.add(this.registerFocusViewAction(viewDescriptor, composite?.name ?? CATEGORIES.View)); + disposables.add(this.registerResetViewLocationAction(viewDescriptor)); this.viewDisposable.set(viewDescriptor, disposables); } } @@ -392,12 +336,225 @@ export class ViewsService extends Disposable implements IViewsService { getViewProgressIndicator(viewId: string): IProgressIndicator | undefined { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(viewId); - if (viewContainer === null) { + if (!viewContainer) { return undefined; } - const view = this.viewPaneContainers.get(viewContainer.id)?.viewPaneContainer?.getView(viewId); - return view?.getProgressIndicator(); + const viewPaneContainer = this.viewPaneContainers.get(viewContainer.id)?.viewPaneContainer; + if (!viewPaneContainer) { + return undefined; + } + + const view = viewPaneContainer.getView(viewId); + if (!view) { + return undefined; + } + + if (viewPaneContainer.isViewMergedWithContainer()) { + return this.getViewContainerProgressIndicator(viewContainer); + } + + return view.getProgressIndicator(); + } + + private getViewContainerProgressIndicator(viewContainer: ViewContainer): IProgressIndicator | undefined { + return this.viewDescriptorService.getViewContainerLocation(viewContainer) === ViewContainerLocation.Sidebar ? this.viewletService.getProgressIndicator(viewContainer.id) : this.panelService.getProgressIndicator(viewContainer.id); + } + + private registerOpenViewContainerAction(viewContainer: ViewContainer): IDisposable { + const disposables = new DisposableStore(); + if (viewContainer.openCommandActionDescriptor) { + let { id, title, mnemonicTitle, keybindings, order } = viewContainer.openCommandActionDescriptor ?? { id: viewContainer.id }; + title = title ?? viewContainer.title; + const that = this; + disposables.add(registerAction2(class OpenViewContainerAction extends Action2 { + constructor() { + super({ + id, + get title(): ICommandActionTitle { + const viewContainerLocation = that.viewDescriptorService.getViewContainerLocation(viewContainer); + if (viewContainerLocation === ViewContainerLocation.Sidebar) { + return { value: localize('show view', "Show {0}", title), original: `Show ${title}` }; + } else { + return { value: localize('toggle view', "Toggle {0}", title), original: `Toggle ${title}` }; + } + }, + category: CATEGORIES.View.value, + precondition: ContextKeyExpr.has(getEnabledViewContainerContextKey(viewContainer.id)), + keybinding: keybindings ? { ...keybindings, weight: KeybindingWeight.WorkbenchContrib } : undefined, + f1: true + }); + } + public async run(serviceAccessor: ServicesAccessor): Promise { + const editorGroupService = serviceAccessor.get(IEditorGroupsService); + const viewDescriptorService = serviceAccessor.get(IViewDescriptorService); + const layoutService = serviceAccessor.get(IWorkbenchLayoutService); + const viewsService = serviceAccessor.get(IViewsService); + const viewContainerLocation = viewDescriptorService.getViewContainerLocation(viewContainer); + switch (viewContainerLocation) { + case ViewContainerLocation.Sidebar: + if (!viewsService.isViewContainerVisible(viewContainer.id) || !layoutService.hasFocus(Parts.SIDEBAR_PART)) { + await viewsService.openViewContainer(viewContainer.id, true); + } else { + editorGroupService.activeGroup.focus(); + } + break; + case ViewContainerLocation.Panel: + if (!viewsService.isViewContainerVisible(viewContainer.id) || !layoutService.hasFocus(Parts.PANEL_PART)) { + await viewsService.openViewContainer(viewContainer.id, true); + } else { + viewsService.closeViewContainer(viewContainer.id); + } + break; + } + } + })); + + if (mnemonicTitle) { + const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer); + disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + command: { + id, + title: mnemonicTitle, + }, + group: defaultLocation === ViewContainerLocation.Sidebar ? '3_views' : '4_panels', + when: ContextKeyExpr.has(getEnabledViewContainerContextKey(viewContainer.id)), + order: order ?? Number.MAX_VALUE + })); + } + } + + return disposables; + } + + private registerOpenViewAction(viewDescriptor: IViewDescriptor): IDisposable { + const disposables = new DisposableStore(); + if (viewDescriptor.openCommandActionDescriptor) { + const title = viewDescriptor.openCommandActionDescriptor.title ?? viewDescriptor.name; + const commandId = viewDescriptor.openCommandActionDescriptor.id; + const that = this; + disposables.add(registerAction2(class OpenViewAction extends Action2 { + constructor() { + super({ + id: commandId, + get title(): ICommandActionTitle { + const viewContainerLocation = that.viewDescriptorService.getViewLocationById(viewDescriptor.id); + if (viewContainerLocation === ViewContainerLocation.Sidebar) { + return { value: localize('show view', "Show {0}", title), original: `Show ${title}` }; + } else { + return { value: localize('toggle view', "Toggle {0}", title), original: `Toggle ${title}` }; + } + }, + category: CATEGORIES.View.value, + precondition: ContextKeyDefinedExpr.create(`${viewDescriptor.id}.active`), + keybinding: viewDescriptor.openCommandActionDescriptor!.keybindings ? { ...viewDescriptor.openCommandActionDescriptor!.keybindings, weight: KeybindingWeight.WorkbenchContrib } : undefined, + f1: true + }); + } + public async run(serviceAccessor: ServicesAccessor): Promise { + const editorGroupService = serviceAccessor.get(IEditorGroupsService); + const viewDescriptorService = serviceAccessor.get(IViewDescriptorService); + const layoutService = serviceAccessor.get(IWorkbenchLayoutService); + const viewsService = serviceAccessor.get(IViewsService); + const contextKeyService = serviceAccessor.get(IContextKeyService); + + const focusedViewId = FocusedViewContext.getValue(contextKeyService); + if (focusedViewId === viewDescriptor.id) { + if (viewDescriptorService.getViewLocationById(viewDescriptor.id) === ViewContainerLocation.Sidebar) { + editorGroupService.activeGroup.focus(); + } else { + layoutService.setPanelHidden(true); + } + } else { + viewsService.openView(viewDescriptor.id, true); + } + } + })); + + if (viewDescriptor.openCommandActionDescriptor.mnemonicTitle) { + const defaultViewContainer = this.viewDescriptorService.getDefaultContainerById(viewDescriptor.id); + if (defaultViewContainer) { + const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(defaultViewContainer); + disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + command: { + id: commandId, + title: viewDescriptor.openCommandActionDescriptor.mnemonicTitle, + }, + group: defaultLocation === ViewContainerLocation.Sidebar ? '3_views' : '4_panels', + when: ContextKeyDefinedExpr.create(`${viewDescriptor.id}.active`), + order: viewDescriptor.openCommandActionDescriptor.order ?? Number.MAX_VALUE + })); + } + } + } + return disposables; + } + + private registerFocusViewAction(viewDescriptor: IViewDescriptor, category?: string | ILocalizedString): IDisposable { + return registerAction2(class FocusViewAction extends Action2 { + constructor() { + super({ + id: viewDescriptor.focusCommand ? viewDescriptor.focusCommand.id : `${viewDescriptor.id}.focus`, + title: { original: `Focus on ${viewDescriptor.name} View`, value: localize({ key: 'focus view', comment: ['{0} indicates the name of the view to be focused.'] }, "Focus on {0} View", viewDescriptor.name) }, + category, + menu: [{ + id: MenuId.CommandPalette, + when: viewDescriptor.when, + }], + keybinding: { + when: ContextKeyExpr.has(`${viewDescriptor.id}.active`), + weight: KeybindingWeight.WorkbenchContrib, + primary: viewDescriptor.focusCommand?.keybindings?.primary, + secondary: viewDescriptor.focusCommand?.keybindings?.secondary, + linux: viewDescriptor.focusCommand?.keybindings?.linux, + mac: viewDescriptor.focusCommand?.keybindings?.mac, + win: viewDescriptor.focusCommand?.keybindings?.win + } + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IViewsService).openView(viewDescriptor.id, true); + } + }); + } + + private registerResetViewLocationAction(viewDescriptor: IViewDescriptor): IDisposable { + return registerAction2(class ResetViewLocationAction extends Action2 { + constructor() { + super({ + id: `${viewDescriptor.id}.resetViewLocation`, + title: { + original: 'Reset Location', + value: localize('resetViewLocation', "Reset Location") + }, + menu: [{ + id: MenuId.ViewTitleContext, + when: ContextKeyExpr.or( + ContextKeyExpr.and( + ContextKeyExpr.equals('view', viewDescriptor.id), + ContextKeyExpr.equals(`${viewDescriptor.id}.defaultViewLocation`, false) + ) + ), + group: '1_hide', + order: 2 + }], + }); + } + run(accessor: ServicesAccessor): void { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const defaultContainer = viewDescriptorService.getDefaultContainerById(viewDescriptor.id)!; + const containerModel = viewDescriptorService.getViewContainerModel(defaultContainer)!; + + // The default container is hidden so we should try to reset its location first + if (defaultContainer.hideIfEmpty && containerModel.visibleViewDescriptors.length === 0) { + const defaultLocation = viewDescriptorService.getDefaultViewContainerLocation(defaultContainer)!; + viewDescriptorService.moveViewContainerToLocation(defaultContainer, defaultLocation); + } + + viewDescriptorService.moveViewsToContainer([viewDescriptor], viewDescriptorService.getDefaultContainerById(viewDescriptor.id)!); + accessor.get(IViewsService).openView(viewDescriptor.id, true); + } + }); } private registerViewletOrPanel(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { @@ -447,11 +604,10 @@ export class ViewsService extends Disposable implements IViewsService { Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( PaneContainerPanel, viewContainer.id, - viewContainer.name, + viewContainer.title, undefined, viewContainer.order, viewContainer.requestedIndex, - viewContainer.focusCommand?.id, )); } @@ -483,7 +639,7 @@ export class ViewsService extends Disposable implements IViewsService { Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( PaneContainerViewlet, viewContainer.id, - viewContainer.name, + viewContainer.title, isString(viewContainer.icon) ? viewContainer.icon : undefined, viewContainer.order, viewContainer.requestedIndex, diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 1669540188..b7d70e5f42 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from 'vs/base/common/actions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IViewDescriptor, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; @@ -12,7 +11,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ViewPaneContainer, ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { Event } from 'vs/base/common/event'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -81,18 +81,6 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { this.getViewsForTarget(newFilterValue).forEach(item => this.viewContainerModel.setVisible(item.id, true)); } - getContextMenuActions(): IAction[] { - const result: IAction[] = Array.from(this.constantViewDescriptors.values()).map(viewDescriptor => ({ - id: `${viewDescriptor.id}.toggleVisibility`, - label: viewDescriptor.name, - checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor.id) - })); - - return result; - } - private getViewsForTarget(target: string[]): IViewDescriptor[] { const views: IViewDescriptor[] = []; for (let i = 0; i < target.length; i++) { @@ -140,7 +128,4 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { abstract getTitle(): string; - getViewsVisibilityActions(): IAction[] { - return []; - } } diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 50e1355d7c..db1a0a96d0 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/style'; -import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { iconForeground, foreground, selectionBackground, focusBorder, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, listHighlightForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -13,7 +13,7 @@ import { createMetaElement } from 'vs/base/browser/dom'; import { isSafari, isStandalone } from 'vs/base/browser/browser'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { +registerThemingParticipant((theme, collector) => { // Foreground const windowForeground = theme.getColor(foreground); diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 9a60955151..079db6d531 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -3,23 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { ToggleSidebarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { URI } from 'vs/base/common/uri'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { AbstractTree } from 'vs/base/browser/ui/tree/abstractTree'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -52,40 +44,6 @@ export abstract class Viewlet extends PaneComposite implements IViewlet { })); } } - - getContextMenuActions(): IAction[] { - const parentActions = [...super.getContextMenuActions()]; - if (parentActions.length) { - parentActions.push(new Separator()); - } - - const toggleSidebarPositionAction = new ToggleSidebarPositionAction(ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService), this.layoutService, this.configurationService); - return [...parentActions, toggleSidebarPositionAction, - { - id: ToggleSidebarVisibilityAction.ID, - label: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"), - enabled: true, - run: () => this.layoutService.setSideBarHidden(true) - }]; - } - - getSecondaryActions(): IAction[] { - const viewVisibilityActions = this.viewPaneContainer.getViewsVisibilityActions(); - const secondaryActions = this.viewPaneContainer.getSecondaryActions(); - if (viewVisibilityActions.length <= 1 || viewVisibilityActions.every(({ enabled }) => !enabled)) { - return secondaryActions; - } - - if (secondaryActions.length === 0) { - return viewVisibilityActions; - } - - return [ - new SubmenuAction('workbench.views', nls.localize('views', "Views"), viewVisibilityActions), - new Separator(), - ...secondaryActions - ]; - } } /** @@ -115,7 +73,7 @@ export class ViewletDescriptor extends CompositeDescriptor { requestedIndex?: number, readonly iconUrl?: URI ) { - super(ctor, id, name, cssClass, order, requestedIndex, id); + super(ctor, id, name, cssClass, order, requestedIndex); } } @@ -152,60 +110,6 @@ export class ViewletRegistry extends CompositeRegistry { getViewlets(): ViewletDescriptor[] { return this.getComposites() as ViewletDescriptor[]; } - } Registry.add(Extensions.Viewlets, new ViewletRegistry()); - -/** - * A reusable action to show a viewlet with a specific id. - */ -export class ShowViewletAction extends Action { - - constructor( - id: string, - name: string, - private readonly viewletId: string, - @IViewletService protected viewletService: IViewletService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService - ) { - super(id, name); - } - - async run(): Promise { - - // Pass focus to viewlet if not open or focused - if (this.otherViewletShowing() || !this.sidebarHasFocus()) { - await this.viewletService.openViewlet(this.viewletId, true); - return; - } - - // Otherwise pass focus to editor group - this.editorGroupService.activeGroup.focus(); - } - - private otherViewletShowing(): boolean { - const activeViewlet = this.viewletService.getActiveViewlet(); - - return !activeViewlet || activeViewlet.getId() !== this.viewletId; - } - - private sidebarHasFocus(): boolean { - const activeViewlet = this.viewletService.getActiveViewlet(); - const activeElement = document.activeElement; - const sidebarPart = this.layoutService.getContainer(Parts.SIDEBAR_PART); - - return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart)); - } -} - -export class CollapseAction extends Action { - // We need a tree getter because the action is sometimes instantiated too early - constructor(treeGetter: () => AsyncDataTree | AbstractTree, enabled: boolean, clazz?: string) { - super('workbench.action.collapse', nls.localize('collapse', "Collapse All"), clazz, enabled, async () => { - const tree = treeGetter(); - tree.collapseAll(); - }); - } -} diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index cc99bdef3d..83e75b7119 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { mark } from 'vs/base/common/performance'; -import { hash } from 'vs/base/common/hash'; -import { domContentLoaded, addDisposableListener, EventType, EventHelper, detectFullscreen, addDisposableThrottledListener } from 'vs/base/browser/dom'; +import { domContentLoaded, detectFullscreen, getCookieValue } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, ConsoleLogService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log'; import { ConsoleLogInAutomationService } from 'vs/platform/log/browser/log'; @@ -24,10 +23,9 @@ import { IFileService, IFileSystemProvider } from 'vs/platform/files/common/file import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import { setFullscreen } from 'vs/base/browser/browser'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; @@ -37,12 +35,11 @@ import { SignService } from 'vs/platform/sign/browser/signService'; import type { IWorkbenchConstructionOptions, IWorkspace, IWorkbench } from 'vs/workbench/workbench.web.api'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; -import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -61,6 +58,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { BrowserWindow } from 'vs/workbench/browser/window'; +import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; class BrowserMain extends Disposable { @@ -83,35 +82,40 @@ class BrowserMain extends Disposable { const services = await this.initServices(); await domContentLoaded(); - mark('willStartWorkbench'); + mark('code/willStartWorkbench'); // Create Workbench - const workbench = new Workbench( - this.domElement, - services.serviceCollection, - services.logService - ); + const workbench = new Workbench(this.domElement, services.serviceCollection, services.logService); // Listeners this.registerListeners(workbench, services.storageService, services.logService); - // Driver - if (this.configuration.driver) { - (async () => this._register(await registerWindowDriver()))(); - } - // Startup const instantiationService = workbench.startup(); + // Window + this._register(instantiationService.createInstance(BrowserWindow)); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); + // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); const lifecycleService = accessor.get(ILifecycleService); + const timerService = accessor.get(ITimerService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, + env: { + async retrievePerformanceMarks() { + await timerService.whenReady(); + + return timerService.getPerformanceMarks(); + } + }, shutdown: () => lifecycleService.shutdown() }; }); @@ -119,46 +123,17 @@ class BrowserMain extends Disposable { private registerListeners(workbench: Workbench, storageService: BrowserStorageService, logService: ILogService): void { - // Layout - const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; - this._register(addDisposableListener(viewport, EventType.RESIZE, () => { - logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); - workbench.layout(); - })); - - // Prevent the back/forward gestures in macOS - this._register(addDisposableListener(this.domElement, EventType.WHEEL, e => e.preventDefault(), { passive: false })); - - // Prevent native context menus in web - this._register(addDisposableListener(this.domElement, EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); - - // Prevent default navigation on drop - this._register(addDisposableListener(this.domElement, EventType.DROP, e => EventHelper.stop(e, true))); - // Workbench Lifecycle this._register(workbench.onBeforeShutdown(event => { if (storageService.hasPendingUpdate) { - logService.warn('Unload veto: pending storage update'); - event.veto(true); // prevent data loss from pending storage update + event.veto(true, 'veto.pendingStorageUpdate'); // prevent data loss from pending storage update } })); - this._register(workbench.onWillShutdown(() => { - storageService.close(); - })); + this._register(workbench.onWillShutdown(() => storageService.close())); this._register(workbench.onShutdown(() => this.dispose())); - - // Fullscreen (Browser) - [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { - this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); - }); - - // Fullscreen (Native) - this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { - setFullscreen(!!detectFullscreen()); - }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IWorkbenchConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { const serviceCollection = new ServiceCollection(); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -166,7 +141,7 @@ class BrowserMain extends Disposable { // CONTRIBUTE IT VIA WORKBENCH.WEB.MAIN.TS AND registerSingleton(). // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - const payload = await this.resolveWorkspaceInitializationPayload(); + const payload = this.resolveWorkspaceInitializationPayload(); // Product const productService: IProductService = { _serviceBrand: undefined, ...product, ...this.configuration.productConfiguration }; @@ -181,9 +156,8 @@ class BrowserMain extends Disposable { const logService = new BufferLogService(getLogLevel(environmentService)); serviceCollection.set(ILogService, logService); - const connectionToken = environmentService.options.connectionToken || this.getCookieValue('vscode-tkn'); - // Remote + const connectionToken = environmentService.options.connectionToken || getCookieValue('vscode-tkn'); const remoteAuthorityResolverService = new RemoteAuthorityResolverService(connectionToken, this.configuration.resourceUriProvider); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); @@ -212,7 +186,7 @@ class BrowserMain extends Disposable { serviceCollection.set(IWorkspaceContextService, service); // Configuration - serviceCollection.set(IConfigurationService, service); + serviceCollection.set(IWorkbenchConfigurationService, service); return service; }), @@ -239,14 +213,16 @@ class BrowserMain extends Disposable { serviceCollection.set(IUserDataInitializationService, userDataInitializationService); if (await userDataInitializationService.requiresInitialization()) { - mark('willInitRequiredUserData'); + mark('code/willInitRequiredUserData'); + // Initialize required resources - settings & global state await userDataInitializationService.initializeRequiredResources(); // Important: Reload only local user configuration after initializing // Reloading complete configuraiton blocks workbench until remote configuration is loaded. await configurationService.reloadLocalUserConfiguration(); - mark('didInitRequiredUserData'); + + mark('code/didInitRequiredUserData'); } return { serviceCollection, configurationService, logService, storageService }; @@ -261,8 +237,9 @@ class BrowserMain extends Disposable { try { indexedDBLogProvider = await indexedDB.createFileSystemProvider(logsPath.scheme, INDEXEDDB_LOGS_OBJECT_STORE); } catch (error) { - console.error(error); + onUnexpectedError(error); } + if (indexedDBLogProvider) { fileService.registerProvider(logsPath.scheme, indexedDBLogProvider); } else { @@ -279,6 +256,7 @@ class BrowserMain extends Disposable { const connection = remoteAgentService.getConnection(); if (connection) { + // Remote file system const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); @@ -289,8 +267,9 @@ class BrowserMain extends Disposable { try { indexedDBUserDataProvider = await indexedDB.createFileSystemProvider(Schemas.userData, INDEXEDDB_USERDATA_OBJECT_STORE); } catch (error) { - console.error(error); + onUnexpectedError(error); } + fileService.registerProvider(Schemas.userData, indexedDBUserDataProvider || new InMemoryFileSystemProvider()); if (indexedDBUserDataProvider) { registerAction2(class ResetUserDataAction extends Action2 { @@ -304,21 +283,22 @@ class BrowserMain extends Disposable { } }); } + async run(accessor: ServicesAccessor): Promise { const dialogService = accessor.get(IDialogService); const hostService = accessor.get(IHostService); const result = await dialogService.confirm({ message: localize('reset user data message', "Would you like to reset your data (settings, keybindings, extensions, snippets and UI State) and reload?") }); + if (result.confirmed) { - await indexedDBUserDataProvider!.reset(); + await indexedDBUserDataProvider?.reset(); } + hostService.reload(); } }); } - - fileService.registerProvider(Schemas.userData, indexedDBUserDataProvider || new InMemoryFileSystemProvider()); } private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { @@ -351,7 +331,7 @@ class BrowserMain extends Disposable { } } - private async resolveWorkspaceInitializationPayload(): Promise { + private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload { let workspace: IWorkspace | undefined = undefined; if (this.configuration.workspaceProvider) { workspace = this.configuration.workspaceProvider.workspace; @@ -364,18 +344,11 @@ class BrowserMain extends Disposable { // Single-folder workspace if (workspace && isFolderToOpen(workspace)) { - const id = hash(workspace.folderUri.toString()).toString(16); - return { id, folder: workspace.folderUri }; + return getSingleFolderWorkspaceIdentifier(workspace.folderUri); } return { id: 'empty-window' }; } - - private getCookieValue(name: string): string | undefined { - const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531 - - return match ? match.pop() : undefined; - } } export function main(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts new file mode 100644 index 0000000000..d3321b0cc3 --- /dev/null +++ b/src/vs/workbench/browser/window.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { setFullscreen } from 'vs/base/browser/browser'; +import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { timeout } from 'vs/base/common/async'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export class BrowserWindow extends Disposable { + + constructor( + @IOpenerService private readonly openerService: IOpenerService, + @ILifecycleService private readonly lifecycleService: BrowserLifecycleService, + @IDialogService private readonly dialogService: IDialogService, + @IHostService private readonly hostService: IHostService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService + ) { + super(); + + this.registerListeners(); + this.create(); + } + + private registerListeners(): void { + + // Lifecycle + this._register(this.lifecycleService.onWillShutdown(() => this.onWillShutdown())); + + // Layout + const viewport = isIOS && window.visualViewport ? window.visualViewport /** Visual viewport */ : window /** Layout viewport */; + this._register(addDisposableListener(viewport, EventType.RESIZE, () => this.onWindowResize())); + + // Prevent the back/forward gestures in macOS + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.WHEEL, e => e.preventDefault(), { passive: false })); + + // Prevent native context menus in web + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); + + // Prevent default navigation on drop + this._register(addDisposableListener(this.layoutService.getWorkbenchContainer(), EventType.DROP, e => EventHelper.stop(e, true))); + + // Fullscreen (Browser) + [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { + this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); + }); + + // Fullscreen (Native) + this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { + setFullscreen(!!detectFullscreen()); + }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); + } + + private onWindowResize(): void { + this.logService.trace(`web.main#${isIOS && window.visualViewport ? 'visualViewport' : 'window'}Resize`); + + this.layoutService.layout(); + } + + private onWillShutdown(): void { + + // Try to detect some user interaction with the workbench + // when shutdown has happened to not show the dialog e.g. + // when navigation takes a longer time. + Event.toPromise(Event.any( + Event.once(domEvent(document.body, EventType.KEY_DOWN, true)), + Event.once(domEvent(document.body, EventType.MOUSE_DOWN, true)) + )).then(async () => { + + // Delay the dialog in case the user interacted + // with the page before it transitioned away + await timeout(3000); + + // This should normally not happen, but if for some reason + // the workbench was shutdown while the page is still there, + // inform the user that only a reload can bring back a working + // state. + const res = await this.dialogService.show( + Severity.Error, + localize('shutdownError', "An unexpected error occurred that requires a reload of this page."), + [ + localize('reload', "Reload") + ], + { + detail: localize('shutdownErrorDetail', "The workbench was unexpectedly disposed while running.") + } + ); + + if (res.choice === 0) { + this.hostService.reload(); + } + }); + } + + private create(): void { + + // Driver + if (this.environmentService.options?.driver) { + (async () => this._register(await registerWindowDriver()))(); + } + + // Handle open calls + this.setupOpenHandlers(); + + // Label formatting + this.registerLabelFormatters(); + } + + private setupOpenHandlers(): void { + + // We need to ignore the `beforeunload` event while + // we handle external links to open specifically for + // the case of application protocols that e.g. invoke + // vscode itself. We do not want to open these links + // in a new window because that would leave a blank + // window to the user, but using `window.location.href` + // will trigger the `beforeunload`. + this.openerService.setDefaultExternalOpener({ + openExternal: async (href: string) => { + if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { + windowOpenNoOpener(href); + } else { + this.lifecycleService.withExpectedUnload(() => window.location.href = href); + } + + return true; + } + }); + } + + private registerLabelFormatters() { + this.labelService.registerFormatter({ + scheme: Schemas.userData, + priority: true, + formatting: { + label: '${scheme}:${path}', + separator: '/', + } + }); + } +} diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 49279b9e0c..4b4a7e06fd 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -33,25 +33,30 @@ import { isStandalone } from 'vs/base/browser/browser'; 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), 'default': true }, + 'workbench.editor.wrapTabs': { + 'type': 'boolean', + 'markdownDescription': nls.localize('wrapTabs', "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when `#workbench.editor.showTabs#` is disabled."), + 'default': false + }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is `false`."), + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is disabled."), 'default': false }, 'workbench.editor.highlightModifiedTabs': { 'type': 'boolean', - 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is `false`."), + 'markdownDescription': nls.localize('highlightModifiedTabs', "Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is disabled."), 'default': false }, - 'workbench.editor.tabDecorations.badges': { + 'workbench.editor.decorations.badges': { 'type': 'boolean', - 'markdownDescription': nls.localize('tabDecorations.badges', "Controls whether editor file decorations should use badges."), - 'default': true + 'markdownDescription': nls.localize('decorations.badges', "Controls whether editor file decorations should use badges."), + 'default': false }, - 'workbench.editor.tabDecorations.colors': { + 'workbench.editor.decorations.colors': { 'type': 'boolean', - 'markdownDescription': nls.localize('tabDecorations.colors', "Controls whether editor file decorations should use colors."), - 'default': true + 'markdownDescription': nls.localize('decorations.colors', "Controls whether editor file decorations should use colors."), + 'default': false }, 'workbench.editor.labelFormat': { 'type': 'string', @@ -85,7 +90,7 @@ import { isStandalone } from 'vs/base/browser/browser'; 'type': 'string', 'enum': ['left', 'right', 'off'], 'default': 'right', - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'editorTabCloseButton' }, "Controls the position of the editor's tabs close buttons, or disables them when set to 'off'. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.tabSizing': { 'type': 'string', @@ -95,7 +100,7 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('workbench.editor.tabSizing.fit', "Always keep tabs large enough to show the full editor label."), nls.localize('workbench.editor.tabSizing.shrink', "Allow tabs to get smaller when the available space is not enough to show all tabs at once.") ], - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'tabSizing' }, "Controls the sizing of editor tabs. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.pinnedTabSizing': { 'type': 'string', @@ -106,7 +111,7 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('workbench.editor.pinnedTabSizing.compact', "A pinned tab will show in a compact form with only icon or first letter of the editor name."), nls.localize('workbench.editor.pinnedTabSizing.shrink', "A pinned tab shrinks to a compact fixed size showing parts of the editor name.") ], - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is `false`.") + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'pinnedTabSizing' }, "Controls the sizing of pinned editor tabs. Pinned tabs are sorted to the beginning of all opened tabs and typically do not close until unpinned. This value is ignored when `#workbench.editor.showTabs#` is disabled.") }, 'workbench.editor.splitSizing': { 'type': 'string', @@ -140,7 +145,12 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'workbench.editor.enablePreviewFromQuickOpen': { 'type': 'boolean', - 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing)."), + 'markdownDescription': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled."), + 'default': false + }, + 'workbench.editor.enablePreviewFromCodeNavigation': { + 'type': 'boolean', + 'markdownDescription': nls.localize('enablePreviewFromCodeNavigation', "Controls whether editors remain in preview when a code navigation is started from them. Preview editors do not keep open and are reused until explicitly set to be kept open (e.g. via double click or editing). This value is ignored when `#workbench.editor.enablePreview#` is disabled."), 'default': false }, 'workbench.editor.closeOnFileDelete': { @@ -325,8 +335,8 @@ import { isStandalone } from 'vs/base/browser/browser'; nls.localize('activeFolderLong', "`\${activeFolderLong}`: the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)."), nls.localize('folderName', "`\${folderName}`: name of the workspace folder the file is contained in (e.g. myFolder)."), nls.localize('folderPath', "`\${folderPath}`: file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)."), - nls.localize('rootName', "`\${rootName}`: name of the workspace (e.g. myFolder or myWorkspace)."), - nls.localize('rootPath', "`\${rootPath}`: file path of the workspace (e.g. /Users/Development/myWorkspace)."), + nls.localize('rootName', "`\${rootName}`: name of the opened workspace or folder (e.g. myFolder or myWorkspace)."), + nls.localize('rootPath', "`\${rootPath}`: file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)."), nls.localize('appName', "`\${appName}`: e.g. VS Code."), nls.localize('remoteName', "`\${remoteName}`: e.g. SSH"), nls.localize('dirty', "`\${dirty}`: a dirty indicator if the active editor is dirty."), @@ -362,17 +372,17 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'window.menuBarVisibility': { 'type': 'string', - 'enum': ['default', 'visible', 'toggle', 'hidden', 'compact'], - 'enumDescriptions': [ - nls.localize('window.menuBarVisibility.default', "Menu is only hidden in full screen mode."), - nls.localize('window.menuBarVisibility.visible', "Menu is always visible even in full screen mode."), - nls.localize('window.menuBarVisibility.toggle', "Menu is hidden but can be displayed via Alt key."), + 'enum': ['classic', 'visible', 'toggle', 'hidden', 'compact'], + 'markdownEnumDescriptions': [ + nls.localize('window.menuBarVisibility.classic', "Menu is displayed at the top of the window and only hidden in full screen mode."), + nls.localize('window.menuBarVisibility.visible', "Menu is always visible at the top of the window even in full screen mode."), + nls.localize('window.menuBarVisibility.toggle', "Menu is hidden but can be displayed at the top of the window via Alt key."), nls.localize('window.menuBarVisibility.hidden', "Menu is always hidden."), nls.localize('window.menuBarVisibility.compact', "Menu is displayed as a compact button in the sidebar. This value is ignored when `#window.titleBarStyle#` is `native`.") ], - 'default': isWeb ? 'compact' : 'default', + 'default': isWeb ? 'compact' : 'classic', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('menuBarVisibility', "Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and a single press of the Alt key will show it. By default, the menu bar will be visible, unless the window is full screen."), + 'description': nls.localize('menuBarVisibility', "Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and a single press of the Alt key will show it. A setting of 'compact' will move the menu into the sidebar."), 'included': isWindows || isLinux || isWeb }, 'window.enableMenuBarMnemonics': { @@ -473,7 +483,7 @@ import { isStandalone } from 'vs/base/browser/browser'; }, 'zenMode.restore': { 'type': 'boolean', - 'default': false, + 'default': true, 'description': nls.localize('zenMode.restore', "Controls whether a window should restore to zen mode if it was exited in zen mode.") }, 'zenMode.silentNotifications': { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 1c2f03169e..1b16929354 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -8,7 +8,7 @@ import 'vs/workbench/browser/style'; import { localize } from 'vs/nls'; import { Emitter, setGlobalLeakWarningThreshold } from 'vs/base/common/event'; import { runWhenIdle } from 'vs/base/common/async'; -import { getZoomLevel, isFirefox, isSafari, isChrome } from 'vs/base/browser/browser'; +import { getZoomLevel, isFirefox, isSafari, isChrome, getPixelRatio } from 'vs/base/browser/browser'; import { mark } from 'vs/base/common/performance'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -180,10 +180,17 @@ export class Workbench extends Layout { // Layout Service serviceCollection.set(IWorkbenchLayoutService, this); - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. - // CONTRIBUTE IT VIA WORKBENCH.DESKTOP.MAIN.TS AND registerSingleton(). - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // + // NOTE: Please do NOT register services here. Use `registerSingleton()` + // from `workbench.common.main.ts` if the service is shared between + // native and web or `workbench.sandbox.main.ts` if the service + // is native only. + // + // DO NOT add services to `workbench.desktop.main.ts`, always add + // to `workbench.sandbox.main.ts` to support our Electron sandbox + // + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // All Contributed Services const contributedServices = getSingletonServiceDescriptors(); @@ -287,7 +294,7 @@ export class Workbench extends Layout { } } - readFontInfo(BareFontInfo.createFromRawSettings(configurationService.getValue('editor'), getZoomLevel())); + readFontInfo(BareFontInfo.createFromRawSettings(configurationService.getValue('editor'), getZoomLevel(), getPixelRatio())); } private storeFontInfo(storageService: IStorageService): void { @@ -338,10 +345,11 @@ export class Workbench extends Layout { // Create Parts [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, - { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, - { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, + // Use role 'none' for some parts to make screen readers less chatty #114892 + { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, + { id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } }, - { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', positionToString(this.state.panel.position)] }, + { id: Parts.PANEL_PART, role: 'none', classes: ['panel', positionToString(this.state.panel.position)] }, { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] } ].forEach(({ id, role, classes, options }) => { const partContainer = this.createPart(id, role, classes); @@ -415,10 +423,10 @@ export class Workbench extends Layout { }, 2500); // Telemetry: startup metrics - mark('didStartWorkbench'); + mark('code/didStartWorkbench'); // Perf reporting (devtools) - performance.measure('perf: workbench create & restore', 'didLoadWorkbenchMain', 'didStartWorkbench'); + performance.measure('perf: workbench create & restore', 'code/didLoadWorkbenchMain', 'code/didStartWorkbench'); } } } diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index cd02363b11..dd0b3b068d 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -128,5 +128,6 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR export const CATEGORIES = { View: { value: localize('view', "View"), original: 'View' }, Help: { value: localize('help', "Help"), original: 'Help' }, + Preferences: { value: localize('preferences', "Preferences"), original: 'Preferences' }, Developer: { value: localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"), original: 'Developer' } }; diff --git a/src/vs/workbench/common/composite.ts b/src/vs/workbench/common/composite.ts index a900178bda..aacd18de44 100644 --- a/src/vs/workbench/common/composite.ts +++ b/src/vs/workbench/common/composite.ts @@ -18,6 +18,11 @@ export interface IComposite { */ readonly onDidBlur: Event; + /** + * Returns true if the composite has focus. + */ + hasFocus(): boolean; + /** * Returns the unique identifier of this composite. */ diff --git a/src/vs/workbench/common/dialogs.ts b/src/vs/workbench/common/dialogs.ts index 4aa245cec4..eb72886c63 100644 --- a/src/vs/workbench/common/dialogs.ts +++ b/src/vs/workbench/common/dialogs.ts @@ -18,6 +18,7 @@ export interface IDialogHandle { } export interface IDialogsModel { + readonly onDidShowDialog: Event; readonly dialogs: IDialogViewItem[]; @@ -26,6 +27,7 @@ export interface IDialogsModel { } export class DialogsModel extends Disposable implements IDialogsModel { + readonly dialogs: IDialogViewItem[] = []; private readonly _onDidShowDialog = this._register(new Emitter()); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 033b0eda6f..937f008503 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorPane extends IComposite { /** * The assigned options of the editor. */ - readonly options: EditorOptions | undefined; + readonly options: IEditorOptions | undefined; /** * The assigned group this editor is showing in. @@ -186,7 +186,7 @@ export interface IFileEditorInputFactory { isFileEditorInput(obj: unknown): obj is IFileEditorInput; } -interface ICustomEditorInputFactory { +export interface ICustomEditorInputFactory { createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise; canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean; } @@ -1215,7 +1215,7 @@ export interface IEditorOpenContext { * An editor is new for a group if it was not part of the group before and * otherwise was already opened in the group and just became the active editor. * - * This hint can e.g. be used to decide wether to restore view state or not. + * This hint can e.g. be used to decide whether to restore view state or not. */ newInGroup?: boolean; } @@ -1257,14 +1257,15 @@ export interface IEditorCloseEvent extends IEditorIdentifier { export type GroupIdentifier = number; export interface IWorkbenchEditorConfiguration { - workbench: { - editor: IEditorPartConfiguration, - iconTheme: string; + workbench?: { + editor?: IEditorPartConfiguration, + iconTheme?: string; }; } interface IEditorPartConfiguration { showTabs?: boolean; + wrapTabs?: boolean; scrollToSwitchTabs?: boolean; highlightModifiedTabs?: boolean; tabCloseButton?: 'left' | 'right' | 'off'; @@ -1275,6 +1276,7 @@ interface IEditorPartConfiguration { showIcons?: boolean; enablePreview?: boolean; enablePreviewFromQuickOpen?: boolean; + enablePreviewFromCodeNavigation?: boolean; closeOnFileDelete?: boolean; openPositioning?: 'left' | 'right' | 'first' | 'last'; openSideBySideDirection?: 'right' | 'down'; @@ -1290,7 +1292,7 @@ interface IEditorPartConfiguration { value?: number; perEditorGroup?: boolean; }; - tabDecorations?: { + decorations?: { badges?: boolean; colors?: boolean; } @@ -1332,7 +1334,9 @@ class EditorResourceAccessorImpl { /** * The original URI of an editor is the URI that was used originally to open * the editor and should be used whenever the URI is presented to the user, - * e.g. as a label. + * e.g. as a label together with utility methods such as `ResourceLabel` or + * `ILabelService` that can turn this original URI into the best form for + * presenting. * * In contrast, the canonical URI (#getCanonicalUri) may be different and should * be used whenever the URI is used to e.g. compare with other editors or when @@ -1581,7 +1585,7 @@ export function computeEditorAriaLabel(input: IEditorInput, index: number | unde ariaLabel = localize('preview', "{0}, preview", ariaLabel); } - if (group && group.isSticky(index ?? input)) { + if (group?.isSticky(index ?? input)) { ariaLabel = localize('pinned', "{0}, pinned", ariaLabel); } diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index a11ddc1561..85b7831842 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -13,6 +13,7 @@ import { dirname } from 'vs/base/common/resources'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; +import { withNullAsUndefined } from 'vs/base/common/types'; /** * The base editor input for the diff editor. It is made up of two editor inputs, the original version @@ -110,21 +111,18 @@ export class DiffEditorInput extends SideBySideEditorInput { private async createModel(): Promise { // Join resolve call over two inputs and build diff editor model - const models = await Promise.all([ + const [originalEditorModel, modifiedEditorModel] = await Promise.all([ this.originalInput.resolve(), this.modifiedInput.resolve() ]); - const originalEditorModel = models[0]; - const modifiedEditorModel = models[1]; - // If both are text models, return textdiffeditor model if (modifiedEditorModel instanceof BaseTextEditorModel && originalEditorModel instanceof BaseTextEditorModel) { return new TextDiffEditorModel(originalEditorModel, modifiedEditorModel); } // Otherwise return normal diff model - return new DiffEditorModel(originalEditorModel, modifiedEditorModel); + return new DiffEditorModel(withNullAsUndefined(originalEditorModel), withNullAsUndefined(modifiedEditorModel)); } matches(otherInput: unknown): boolean { diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index 8d8245ae8a..ba1ef2691f 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -12,13 +12,13 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; */ export class DiffEditorModel extends EditorModel { - protected readonly _originalModel: IEditorModel | null; - get originalModel(): IEditorModel | null { return this._originalModel; } + protected readonly _originalModel: IEditorModel | undefined; + get originalModel(): IEditorModel | undefined { return this._originalModel; } - protected readonly _modifiedModel: IEditorModel | null; - get modifiedModel(): IEditorModel | null { return this._modifiedModel; } + protected readonly _modifiedModel: IEditorModel | undefined; + get modifiedModel(): IEditorModel | undefined { return this._modifiedModel; } - constructor(originalModel: IEditorModel | null, modifiedModel: IEditorModel | null) { + constructor(originalModel: IEditorModel | undefined, modifiedModel: IEditorModel | undefined) { super(); this._originalModel = originalModel; @@ -28,7 +28,7 @@ export class DiffEditorModel extends EditorModel { async load(): Promise { await Promise.all([ this._originalModel?.load(), - this._modifiedModel?.load(), + this._modifiedModel?.load() ]); return this; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 438b143348..9cd616a558 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -725,12 +725,19 @@ export class EditorGroup extends Disposable { clone(): EditorGroup { const group = this.instantiationService.createInstance(EditorGroup, undefined); + + // Copy over group properties group.editors = this.editors.slice(0); group.mru = this.mru.slice(0); group.preview = this.preview; group.active = this.active; group.sticky = this.sticky; + // Ensure to register listeners for each editor + for (const editor of group.editors) { + group.registerEditorListeners(editor); + } + return group; } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index b0efdf96e0..6599f76d00 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -97,7 +97,7 @@ export class ResourceEditorInput extends AbstractTextResourceEditorInput impleme ref.dispose(); this.modelReference = undefined; - throw new Error(`Unexpected model for ResourcEditorInput: ${this.resource}`); + throw new Error(`Unexpected model for ResourceEditorInput: ${this.resource}`); } this.cachedModel = model; diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/resourceEditorModel.ts index e1caa2daf7..64a4c07086 100644 --- a/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/src/vs/workbench/common/editor/resourceEditorModel.ts @@ -23,7 +23,7 @@ export class ResourceEditorModel extends BaseTextEditorModel { dispose(): void { - // TODO@Joao: force this class to dispose the underlying model + // force this class to dispose the underlying model if (this.textEditorModelHandle) { this.modelService.destroyModel(this.textEditorModelHandle); } diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index db26f6e31e..ea4b7ffda1 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -14,14 +14,14 @@ import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; */ export class TextDiffEditorModel extends DiffEditorModel { - protected readonly _originalModel: BaseTextEditorModel | null; - get originalModel(): BaseTextEditorModel | null { return this._originalModel; } + protected readonly _originalModel: BaseTextEditorModel | undefined; + get originalModel(): BaseTextEditorModel | undefined { return this._originalModel; } - protected readonly _modifiedModel: BaseTextEditorModel | null; - get modifiedModel(): BaseTextEditorModel | null { return this._modifiedModel; } + protected readonly _modifiedModel: BaseTextEditorModel | undefined; + get modifiedModel(): BaseTextEditorModel | undefined { return this._modifiedModel; } - private _textDiffEditorModel: IDiffEditorModel | null = null; - get textDiffEditorModel(): IDiffEditorModel | null { return this._textDiffEditorModel; } + private _textDiffEditorModel: IDiffEditorModel | undefined = undefined; + get textDiffEditorModel(): IDiffEditorModel | undefined { return this._textDiffEditorModel; } constructor(originalModel: BaseTextEditorModel, modifiedModel: BaseTextEditorModel) { super(originalModel, modifiedModel); @@ -73,7 +73,7 @@ export class TextDiffEditorModel extends DiffEditorModel { // inside. We never created the two models (original and modified) so we can not dispose // them without sideeffects. Rather rely on the models getting disposed when their related // inputs get disposed from the diffEditorInput. - this._textDiffEditorModel = null; + this._textDiffEditorModel = undefined; super.dispose(); } diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 4a4d801593..dda2bf15b4 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -18,7 +18,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; */ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel, IModeSupport { - protected textEditorModelHandle: URI | null = null; + protected textEditorModelHandle: URI | undefined = undefined; private createdEditorModel: boolean | undefined; @@ -52,7 +52,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel private registerModelDisposeListener(model: ITextModel): void { this.modelDisposeListener.value = model.onWillDispose(() => { - this.textEditorModelHandle = null; // make sure we do not dispose code editor model again + this.textEditorModelHandle = undefined; // make sure we do not dispose code editor model again this.dispose(); }); } @@ -178,7 +178,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel this.modelService.destroyModel(this.textEditorModelHandle); } - this.textEditorModelHandle = null; + this.textEditorModelHandle = undefined; this.createdEditorModel = false; super.dispose(); diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index bdcc7cfdf3..4561cf2e72 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -199,14 +199,14 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem } // Normal save - return this.doSave(group, options, false); + return this.doSave(options, false); } saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSave(group, options, true); + return this.doSave(options, true); } - private async doSave(group: GroupIdentifier, options: ISaveOptions | undefined, saveAs: boolean): Promise { + private async doSave(options: ISaveOptions | undefined, saveAs: boolean): Promise { // Save / Save As let target: URI | undefined; @@ -220,8 +220,13 @@ export abstract class AbstractTextResourceEditorInput extends EditorInput implem return undefined; // save cancelled } - // If the target is a different resource, return with a new editor input - if (!isEqual(target, this.preferredResource)) { + // If this save operation results in a new editor, either + // because it was saved to disk (e.g. from untitled) or + // through an explicit "Save As", make sure to replace it. + if ( + target.scheme !== this.resource.scheme || + (saveAs && !isEqual(target, this.preferredResource)) + ) { return this.editorService.createEditorInput({ resource: target }); } diff --git a/src/vs/workbench/common/panel.ts b/src/vs/workbench/common/panel.ts index ed99ea8017..b1396b582e 100644 --- a/src/vs/workbench/common/panel.ts +++ b/src/vs/workbench/common/panel.ts @@ -9,5 +9,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const ActivePanelContext = new RawContextKey('activePanel', ''); export const PanelFocusContext = new RawContextKey('panelFocus', false); export const PanelPositionContext = new RawContextKey('panelPosition', 'bottom'); +export const PanelVisibleContext = new RawContextKey('panelVisible', false); +export const PanelMaximizedContext = new RawContextKey('panelMaximized', false); export interface IPanel extends IComposite { } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index c0c73fa56d..fa75d676a6 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -339,7 +339,7 @@ export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { dark: '#FFFFFF', light: '#FFFFFF', hc: '#FFFFFF' -}, nls.localize('statusBarForeground', "Status bar foreground color when a workspace is opened. The status bar is shown in the bottom of the window.")); +}, nls.localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { dark: STATUS_BAR_FOREGROUND, @@ -351,7 +351,7 @@ export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', light: '#007ACC', hc: null -}, nls.localize('statusBarBackground', "Status bar background color when a workspace is opened. The status bar is shown in the bottom of the window.")); +}, nls.localize('statusBarBackground', "Status bar background color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_BACKGROUND = registerColor('statusBar.noFolderBackground', { dark: '#68217A', diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 8f62c8205a..14414113d1 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,9 +27,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; -export const testViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.')); +import { CancellationToken } from 'vs/base/common/cancellation'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); @@ -38,38 +36,90 @@ export namespace Extensions { export const ViewsRegistry = 'workbench.registry.view'; } -export enum ViewContainerLocation { +export const enum ViewContainerLocation { Sidebar, Panel, Dialog // {{SQL CARBON EDIT}} } +export function ViewContainerLocationToString(viewContainerLocation: ViewContainerLocation) { + switch (viewContainerLocation) { + case ViewContainerLocation.Sidebar: return 'sidebar'; + case ViewContainerLocation.Panel: return 'panel'; + } + return ''; +} + +type OpenCommandActionDescriptor = { + readonly id: string; + readonly title?: string; + readonly mnemonicTitle?: string; + readonly order?: number; + readonly keybindings?: IKeybindings & { when?: ContextKeyExpression }; +}; + +/** + * View Container Contexts + */ +export function getEnabledViewContainerContextKey(viewContainerId: string): string { return `viewContainer.${viewContainerId}.enabled`; } + export interface IViewContainerDescriptor { + /** + * The id of the view container + */ readonly id: string; - readonly name: string; + /** + * The title of the view container + */ + readonly title: string; + /** + * Icon representation of the View container + */ + readonly icon?: ThemeIcon | URI; + + /** + * Order of the view container. + */ + readonly order?: number; + + /** + * IViewPaneContainer Ctor to instantiate + */ readonly ctorDescriptor: SyncDescriptor; + /** + * Descriptor for open view container command + * If not provided, view container info (id, title) is used. + * + * Note: To prevent registering open command, use `donotRegisterOpenCommand` flag while registering the view container + */ + readonly openCommandActionDescriptor?: OpenCommandActionDescriptor; + + /** + * Storage id to use to store the view container state. + * If not provided, it will be derived. + */ readonly storageId?: string; - readonly icon?: ThemeIcon | URI; + /** + * If enabled, view container is not shown if it has no active views. + */ + readonly hideIfEmpty?: boolean; + + /** + * Id of the extension that contributed the view container + */ + readonly extensionId?: ExtensionIdentifier; readonly alwaysUseContainerInfo?: boolean; - readonly focusCommand?: { id: string, keybindings?: IKeybindings }; - readonly viewOrderDelegate?: ViewOrderDelegate; - readonly hideIfEmpty?: boolean; - - readonly extensionId?: ExtensionIdentifier; - readonly rejectAddedViews?: boolean; - readonly order?: number; - requestedIndex?: number; } @@ -98,7 +148,7 @@ export interface IViewContainersRegistry { * * @returns the registered ViewContainer. */ - registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, location: ViewContainerLocation, isDefault?: boolean): ViewContainer; + registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, location: ViewContainerLocation, options?: { isDefault?: boolean, donotRegisterOpenCommand?: boolean }): ViewContainer; /** * Deregisters the given view container @@ -135,6 +185,11 @@ interface ViewOrderDelegate { export interface ViewContainer extends IViewContainerDescriptor { } +interface RelaxedViewContainer extends ViewContainer { + + openCommandActionDescriptor?: OpenCommandActionDescriptor; +} + class ViewContainersRegistryImpl extends Disposable implements IViewContainersRegistry { private readonly _onDidRegister = this._register(new Emitter<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>()); @@ -150,16 +205,17 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe return flatten([...this.viewContainers.values()]); } - registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, viewContainerLocation: ViewContainerLocation, isDefault?: boolean): ViewContainer { + registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, viewContainerLocation: ViewContainerLocation, options?: { isDefault?: boolean, donotRegisterOpenCommand?: boolean }): ViewContainer { const existing = this.get(viewContainerDescriptor.id); if (existing) { return existing; } - const viewContainer: ViewContainer = viewContainerDescriptor; + const viewContainer: RelaxedViewContainer = viewContainerDescriptor; + viewContainer.openCommandActionDescriptor = options?.donotRegisterOpenCommand ? undefined : (viewContainer.openCommandActionDescriptor ?? { id: viewContainer.id }); const viewContainers = getOrSet(this.viewContainers, viewContainerLocation, []); viewContainers.push(viewContainer); - if (isDefault) { + if (options?.isDefault) { this.defaultViewContainers.push(viewContainer); } this._onDidRegister.fire({ viewContainer, viewContainerLocation }); @@ -237,6 +293,8 @@ export interface IViewDescriptor { readonly group?: string; readonly remoteAuthority?: string | string[]; + + readonly openCommandActionDescriptor?: OpenCommandActionDescriptor } export interface IViewDescriptorRef { @@ -257,9 +315,12 @@ export interface IAddedViewDescriptorState { export interface IViewContainerModel { + readonly viewContainer: ViewContainer; + readonly title: string; readonly icon: ThemeIcon | URI | undefined; - readonly onDidChangeContainerInfo: Event<{ title?: boolean, icon?: boolean }>; + readonly keybindingId: string | undefined; + readonly onDidChangeContainerInfo: Event<{ title?: boolean, icon?: boolean, keybindingId?: boolean }>; readonly allViewDescriptors: ReadonlyArray; readonly onDidChangeAllViewDescriptors: Event<{ added: ReadonlyArray, removed: ReadonlyArray }>; @@ -506,7 +567,7 @@ export interface IViewsService { * View Contexts */ export const FocusedViewContext = new RawContextKey('focusedView', ''); -export function getVisbileViewContextKey(viewId: string): string { return `${viewId}.visible`; } +export function getVisbileViewContextKey(viewId: string): string { return `view.${viewId}.visible`; } export const IViewDescriptorService = createDecorator('viewDescriptorService'); @@ -687,26 +748,50 @@ export class ResolvableTreeItem implements ITreeItem { command?: Command; children?: ITreeItem[]; accessibilityInformation?: IAccessibilityInformation; - resolve: () => Promise; + resolve: (token: CancellationToken) => Promise; private resolved: boolean = false; private _hasResolve: boolean = false; - constructor(treeItem: ITreeItem, resolve?: (() => Promise)) { + constructor(treeItem: ITreeItem, resolve?: ((token: CancellationToken) => Promise)) { mixin(this, treeItem); this._hasResolve = !!resolve; - this.resolve = async () => { + this.resolve = async (token: CancellationToken) => { if (resolve && !this.resolved) { - const resolvedItem = await resolve(); + const resolvedItem = await resolve(token); if (resolvedItem) { - // Resolvable elements. Currently only tooltip. - this.tooltip = resolvedItem.tooltip; + // Resolvable elements. Currently tooltip and command. + this.tooltip = this.tooltip ?? resolvedItem.tooltip; + this.command = this.command ?? resolvedItem.command; } } - this.resolved = true; + if (!token.isCancellationRequested) { + this.resolved = true; + } }; } get hasResolve(): boolean { return this._hasResolve; } + public resetResolve() { + this.resolved = false; + } + public asTreeItem(): ITreeItem { + return { + handle: this.handle, + parentHandle: this.parentHandle, + collapsibleState: this.collapsibleState, + label: this.label, + description: this.description, + icon: this.icon, + iconDark: this.iconDark, + themeIcon: this.themeIcon, + resourceUri: this.resourceUri, + tooltip: this.tooltip, + contextValue: this.contextValue, + command: this.command, + children: this.children, + accessibilityInformation: this.accessibilityInformation + }; + } } export interface ITreeViewDataProvider { @@ -738,5 +823,6 @@ export interface IViewPaneContainer { getActionViewItem(action: IAction): IActionViewItem | undefined; getActionsContext(): unknown; getView(viewId: string): IView | undefined; + toggleViewVisibility(viewId: string): void; saveState(): void; } diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index 9fe8389c7d..bce1fe7eea 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -16,6 +16,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Promises } from 'vs/base/common/async'; export class BackupRestorer implements IWorkbenchContribution { @@ -29,7 +31,8 @@ export class BackupRestorer implements IWorkbenchContribution { @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, + @ILogService private readonly logService: ILogService ) { this.restoreBackups(); } @@ -46,7 +49,11 @@ export class BackupRestorer implements IWorkbenchContribution { // Some failed to restore or were not opened at all so we open and resolve them manually if (unresolvedBackups.length > 0) { - await this.doOpenEditors(unresolvedBackups); + try { + await this.doOpenEditors(unresolvedBackups); + } catch (error) { + this.logService.error(error); + } return this.doResolveOpenedBackups(unresolvedBackups); } @@ -57,7 +64,7 @@ export class BackupRestorer implements IWorkbenchContribution { private async doResolveOpenedBackups(backups: URI[]): Promise { const unresolvedBackups: URI[] = []; - await Promise.all(backups.map(async backup => { + await Promises.settled(backups.map(async backup => { const openedEditor = this.findEditorByResource(backup); if (openedEditor) { try { @@ -76,7 +83,7 @@ export class BackupRestorer implements IWorkbenchContribution { private findEditorByResource(resource: URI): IEditorInput | undefined { for (const editor of this.editorService.editors) { const customFactory = Registry.as(EditorExtensions.EditorInputFactories).getCustomEditorInputFactory(resource.scheme); - if (customFactory && customFactory.canResolveBackup(editor, resource)) { + if (customFactory?.canResolveBackup(editor, resource)) { return editor; } else if (isEqual(editor.resource, resource)) { return editor; @@ -88,7 +95,7 @@ export class BackupRestorer implements IWorkbenchContribution { private async doOpenEditors(resources: URI[]): Promise { const hasOpenedEditors = this.editorService.visibleEditors.length > 0; - const inputs = await Promise.all(resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors))); + const inputs = await Promises.settled(resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors))); // Open all remaining backups as editors and resolve them to load their backups await this.editorService.openEditors(inputs); diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index 7474c26f01..c922085992 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -18,7 +18,7 @@ export abstract class BackupTracker extends Disposable { private readonly mapWorkingCopyToContentVersion = new Map(); // A map of scheduled pending backups for working copies - private readonly pendingBackups = new Map(); + protected readonly pendingBackups = new Map(); constructor( protected readonly backupFileService: IBackupFileService, @@ -43,7 +43,7 @@ export abstract class BackupTracker extends Disposable { this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); // Lifecycle (handled in subclasses) - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason), 'veto.backups')); } private onDidRegister(workingCopy: IWorkingCopy): void { diff --git a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts index a055ab8c2f..6a81fb19d0 100644 --- a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts @@ -9,7 +9,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { ConfirmResult, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ConfirmResult, IFileDialogService, IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isMacintosh } from 'vs/base/common/platform'; @@ -20,7 +20,9 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SaveReason } from 'vs/workbench/common/editor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { Promises, raceCancellation } from 'vs/base/common/async'; export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { @@ -47,7 +49,8 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont @INativeHostService private readonly nativeHostService: INativeHostService, @ILogService logService: ILogService, @IEditorService private readonly editorService: IEditorService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IProgressService private readonly progressService: IProgressService ) { super(backupFileService, workingCopyService, logService, lifecycleService); } @@ -81,14 +84,18 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return this.onBeforeShutdownWithoutDirty(); } - protected async onBeforeShutdownWithDirty(reason: ShutdownReason, workingCopies: IWorkingCopy[]): Promise { + protected async onBeforeShutdownWithDirty(reason: ShutdownReason, dirtyWorkingCopies: IWorkingCopy[]): Promise { // If auto save is enabled, save all non-untitled working copies // and then check again for dirty copies if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) { - // Save all files - await this.doSaveAllBeforeShutdown(false /* not untitled */, SaveReason.AUTO); + // Save all dirty working copies + try { + await this.doSaveAllBeforeShutdown(false /* not untitled */, SaveReason.AUTO); + } catch (error) { + this.logService.error(`[backup tracker] error saving dirty working copies: ${error}`); // guard against misbehaving saves, we handle remaining dirty below + } // If we still have dirty working copies, we either have untitled ones or working copies that cannot be saved const remainingDirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; @@ -100,18 +107,18 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont } // Auto save is not enabled - return this.handleDirtyBeforeShutdown(workingCopies, reason); + return this.handleDirtyBeforeShutdown(dirtyWorkingCopies, reason); } - private async handleDirtyBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise { + private async handleDirtyBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[], reason: ShutdownReason): Promise { // Trigger backup if configured let backups: IWorkingCopy[] = []; let backupError: Error | undefined = undefined; if (this.filesConfigurationService.isHotExitEnabled) { try { - backups = await this.backupBeforeShutdown(workingCopies, reason); - if (backups.length === workingCopies.length) { + backups = await this.backupBeforeShutdown(dirtyWorkingCopies, reason); + if (backups.length === dirtyWorkingCopies.length) { return false; // no veto (backup was successful for all working copies) } } catch (error) { @@ -121,7 +128,13 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // we ran a backup but received an error that we show to the user if (backupError) { - this.showErrorDialog(localize('backupTrackerBackupFailed', "One or more dirty editors could not be saved to the back up location."), backupError); + if (this.environmentService.isExtensionDevelopment) { + this.logService.error(`[backup tracker] error creating backups: ${backupError}`); + + return false; // do not block shutdown during extension development (https://github.com/microsoft/vscode/issues/115028) + } + + this.showErrorDialog(localize('backupTrackerBackupFailed', "The following dirty editors could not be saved to the back up location."), dirtyWorkingCopies, backupError); return true; // veto (the backup failed) } @@ -129,21 +142,34 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // since a backup did not happen, we have to confirm for // the working copies that did not successfully backup try { - return await this.confirmBeforeShutdown(workingCopies.filter(workingCopy => !backups.includes(workingCopy))); + return await this.confirmBeforeShutdown(dirtyWorkingCopies.filter(workingCopy => !backups.includes(workingCopy))); } catch (error) { - this.showErrorDialog(localize('backupTrackerConfirmFailed', "One or more dirty editors could not be saved or reverted."), error); + if (this.environmentService.isExtensionDevelopment) { + this.logService.error(`[backup tracker] error saving or reverting dirty working copies: ${error}`); + + return false; // do not block shutdown during extension development (https://github.com/microsoft/vscode/issues/115028) + } + + this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following dirty editors could not be saved or reverted."), dirtyWorkingCopies, error); return true; // veto (save or revert failed) } } - private showErrorDialog(msg: string, error?: Error): void { - this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail: localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again.") }); + private showErrorDialog(msg: string, workingCopies: readonly IWorkingCopy[], error?: Error): void { + const dirtyWorkingCopies = workingCopies.filter(workingCopy => workingCopy.isDirty()); + + const advice = localize('backupErrorDetails', "Try saving or reverting the dirty editors first and then try again."); + const detail = dirtyWorkingCopies.length + ? getFileNamesMessage(dirtyWorkingCopies.map(x => x.name)) + '\n' + advice + : advice; + + this.dialogService.show(Severity.Error, msg, [localize('ok', 'OK')], { detail }); this.logService.error(error ? `[backup tracker] ${msg}: ${error}` : `[backup tracker] ${msg}`); } - private async backupBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise { + private async backupBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[], reason: ShutdownReason): Promise { // When quit is requested skip the confirm callback and attempt to backup all workspaces. // When quit is not requested the confirm callback should be shown when the window being @@ -183,10 +209,20 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont } } - // Perform a backup of all dirty working copies unless a backup already exists + if (!doBackup) { + return []; + } + + return this.doBackupBeforeShutdown(dirtyWorkingCopies); + } + + private async doBackupBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { const backups: IWorkingCopy[] = []; - if (doBackup) { - await Promise.all(workingCopies.map(async workingCopy => { + + await this.withProgressAndCancellation(async token => { + + // Perform a backup of all dirty working copies unless a backup already exists + await Promises.settled(dirtyWorkingCopies.map(async workingCopy => { const contentVersion = this.getContentVersion(workingCopy); // Backup exists @@ -196,48 +232,57 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // Backup does not exist else { - const backup = await workingCopy.backup(CancellationToken.None); + const backup = await workingCopy.backup(token); await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta); backups.push(workingCopy); } })); - } + }, localize('backupBeforeShutdown', "Waiting for dirty editors to backup...")); return backups; } - private async confirmBeforeShutdown(workingCopies: IWorkingCopy[]): Promise { + private async confirmBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { // Save - const confirm = await this.fileDialogService.showSaveConfirm(workingCopies.map(workingCopy => workingCopy.name)); + const confirm = await this.fileDialogService.showSaveConfirm(dirtyWorkingCopies.map(workingCopy => workingCopy.name)); if (confirm === ConfirmResult.SAVE) { const dirtyCountBeforeSave = this.workingCopyService.dirtyCount; - await this.doSaveAllBeforeShutdown(workingCopies, SaveReason.EXPLICIT); + + try { + await this.doSaveAllBeforeShutdown(dirtyWorkingCopies, SaveReason.EXPLICIT); + } catch (error) { + this.logService.error(`[backup tracker] error saving dirty working copies: ${error}`); // guard against misbehaving saves, we handle remaining dirty below + } const savedWorkingCopies = dirtyCountBeforeSave - this.workingCopyService.dirtyCount; - if (savedWorkingCopies < workingCopies.length) { + if (savedWorkingCopies < dirtyWorkingCopies.length) { return true; // veto (save failed or was canceled) } - return this.noVeto(workingCopies); // no veto (dirty saved) + return this.noVeto(dirtyWorkingCopies); // no veto (dirty saved) } // Don't Save else if (confirm === ConfirmResult.DONT_SAVE) { - await this.doRevertAllBeforeShutdown(workingCopies); + try { + await this.doRevertAllBeforeShutdown(dirtyWorkingCopies); + } catch (error) { + this.logService.error(`[backup tracker] error reverting dirty working copies: ${error}`); // do not block the shutdown on errors from revert + } - return this.noVeto(workingCopies); // no veto (dirty reverted) + return this.noVeto(dirtyWorkingCopies); // no veto (dirty reverted) } // Cancel return true; // veto (user canceled) } - private async doSaveAllBeforeShutdown(workingCopies: IWorkingCopy[], reason: SaveReason): Promise; - private async doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; - private async doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { - const workingCopies = Array.isArray(arg1) ? arg1 : this.workingCopyService.dirtyWorkingCopies.filter(workingCopy => { + private doSaveAllBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[], reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { + const dirtyWorkingCopies = Array.isArray(arg1) ? arg1 : this.workingCopyService.dirtyWorkingCopies.filter(workingCopy => { if (arg1 === false && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { return false; // skip untitled unless explicitly included } @@ -245,36 +290,52 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return true; }); - // Skip save participants on shutdown for performance reasons - const saveOptions = { skipSaveParticipants: true, reason }; + return this.withProgressAndCancellation(async () => { - // First save through the editor service if we save all to benefit - // from some extras like switching to untitled dirty editors before saving. - let result: boolean | undefined = undefined; - if (typeof arg1 === 'boolean' || workingCopies.length === this.workingCopyService.dirtyCount) { - result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); - } + // Skip save participants on shutdown for performance reasons + const saveOptions = { skipSaveParticipants: true, reason }; - // If we still have dirty working copies, save those directly - // unless the save was not successful (e.g. cancelled) - if (result !== false) { - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : true)); - } + // First save through the editor service if we save all to benefit + // from some extras like switching to untitled dirty editors before saving. + let result: boolean | undefined = undefined; + if (typeof arg1 === 'boolean' || dirtyWorkingCopies.length === this.workingCopyService.dirtyCount) { + result = await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions }); + } + + // If we still have dirty working copies, save those directly + // unless the save was not successful (e.g. cancelled) + if (result !== false) { + await Promises.settled(dirtyWorkingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : Promise.resolve(true))); + } + }, localize('saveBeforeShutdown', "Waiting for dirty editors to save...")); } - private async doRevertAllBeforeShutdown(workingCopies: IWorkingCopy[]): Promise { + private doRevertAllBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { + return this.withProgressAndCancellation(async () => { - // Soft revert is good enough on shutdown - const revertOptions = { soft: true }; + // Soft revert is good enough on shutdown + const revertOptions = { soft: true }; - // First revert through the editor service if we revert all - if (workingCopies.length === this.workingCopyService.dirtyCount) { - await this.editorService.revertAll(revertOptions); - } + // First revert through the editor service if we revert all + if (dirtyWorkingCopies.length === this.workingCopyService.dirtyCount) { + await this.editorService.revertAll(revertOptions); + } - // If we still have dirty working copies, revert those directly - // unless the revert operation was not successful (e.g. cancelled) - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : undefined)); + // If we still have dirty working copies, revert those directly + // unless the revert operation was not successful (e.g. cancelled) + await Promises.settled(dirtyWorkingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : Promise.resolve())); + }, localize('revertBeforeShutdown', "Waiting for dirty editors to revert...")); + } + + private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string): Promise { + const cts = new CancellationTokenSource(); + + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + cancellable: true, // for issues such as https://github.com/microsoft/vscode/issues/112278 + delay: 800, // delay notification so that it only appears when operation takes a long time + title + }, () => raceCancellation(promiseFactory(cts.token), cts.token), () => cts.dispose(true)); } private noVeto(backupsToDiscard: IWorkingCopy[]): boolean | Promise { @@ -282,7 +343,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return false; // if editors have not restored, we are not up to speed with backups and thus should not discard them } - return Promise.all(backupsToDiscard.map(workingCopy => this.backupFileService.discardBackup(workingCopy.resource))).then(() => false, () => false); + return Promises.settled(backupsToDiscard.map(workingCopy => this.backupFileService.discardBackup(workingCopy.resource))).then(() => false, () => false); } private async onBeforeShutdownWithoutDirty(): Promise { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts similarity index 52% rename from src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts rename to src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts index 451f713d76..6bb03031b0 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/browser/backupRestorer.test.ts @@ -4,92 +4,52 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as pfs from 'vs/base/node/pfs'; +import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; -import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; -import { workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { InMemoryTestBackupFileService, registerTestResourceEditor, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; +import { DisposableStore } from 'vs/base/common/lifecycle'; -const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); -const backupHome = path.join(userdataDir, 'Backups'); -const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); +suite('BackupRestorer', () => { -const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); -const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); -const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo'); -const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar'); -const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); -const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); - -class TestBackupRestorer extends BackupRestorer { - async doRestoreBackups(): Promise { - return super.doRestoreBackups(); + class TestBackupRestorer extends BackupRestorer { + async doRestoreBackups(): Promise { + return super.doRestoreBackups(); + } } -} -suite.skip('BackupRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydresser these tests are failing due to tabColorMode, should investigate and fix let accessor: TestServiceAccessor; + let disposables = new DisposableStore(); - let disposables: IDisposable[] = []; + const fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); + const barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); + const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); - setup(async () => { - disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TextFileEditor, - TextFileEditor.ID, - 'Text File Editor' - ), - [new SyncDescriptor(FileEditorInput)] - )); - - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); - await pfs.mkdirp(backupHome); - - return pfs.writeFile(workspacesJsonPath, ''); + setup(() => { + disposables.add(registerTestResourceEditor()); }); - teardown(async () => { - dispose(disposables); - disposables = []; - - (accessor.textFileService.files).dispose(); - - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + teardown(() => { + disposables.clear(); }); test('Restore backups', async function () { - this.timeout(20000); - - const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + const backupFileService = new InMemoryTestBackupFileService(); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); - const part = instantiationService.createInstance(EditorPart); + const part = disposables.add(instantiationService.createInstance(EditorPart)); part.create(document.createElement('div')); part.layout(400, 300); @@ -102,18 +62,18 @@ suite.skip('BackupRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydresser await part.whenRestored; - const tracker = instantiationService.createInstance(NativeBackupTracker); + disposables.add(instantiationService.createInstance(BrowserBackupTracker)); const restorer = instantiationService.createInstance(TestBackupRestorer); // Backup 2 normal files and 2 untitled file - await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).createSnapshot(false)); - await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).createSnapshot(false)); + await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); // Verify backups restored and opened as dirty await restorer.doRestoreBackups(); - assert.equal(editorService.count, 4); + assert.strictEqual(editorService.count, 4); assert.ok(editorService.editors.every(editor => editor.isDirty())); let counter = 0; @@ -152,9 +112,6 @@ suite.skip('BackupRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydresser } } - assert.equal(counter, 4); - - part.dispose(); - tracker.dispose(); + assert.strictEqual(counter, 4); }); }); diff --git a/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts new file mode 100644 index 0000000000..7673587bf2 --- /dev/null +++ b/src/vs/workbench/contrib/backup/test/browser/backupTracker.test.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { toResource } from 'vs/base/test/common/utils'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { InMemoryTestBackupFileService, registerTestResourceEditor, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { timeout } from 'vs/base/common/async'; +import { BrowserBackupTracker } from 'vs/workbench/contrib/backup/browser/backupTracker'; + +suite('BackupTracker (browser)', function () { + let accessor: TestServiceAccessor; + + class TestBackupTracker extends BrowserBackupTracker { + + constructor( + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, + @ILogService logService: ILogService, + ) { + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, logService); + } + + protected getBackupScheduleDelay(): number { + return 10; // Reduce timeout for tests + } + } + + async function createTracker(): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, backupFileService: InMemoryTestBackupFileService, instantiationService: IInstantiationService, cleanup: () => void }> { + const backupFileService = new InMemoryTestBackupFileService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IBackupFileService, backupFileService); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + const editorRegistration = registerTestResourceEditor(); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService: EditorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + accessor = instantiationService.createInstance(TestServiceAccessor); + + await part.whenRestored; + + const tracker = instantiationService.createInstance(TestBackupTracker); + + const cleanup = () => { + part.dispose(); + tracker.dispose(); + editorRegistration.dispose(); + }; + + return { accessor, part, tracker, backupFileService, instantiationService, cleanup }; + } + + async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { + const { accessor, cleanup, backupFileService } = await createTracker(); + + const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; + + const untitledModel = await untitledEditor.resolve(); + + if (!untitled?.contents) { + untitledModel.textEditorModel.setValue('Super Good'); + } + + await backupFileService.joinBackupResource(); + + assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), true); + + untitledModel.dispose(); + + await backupFileService.joinDiscardBackup(); + + assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), false); + + cleanup(); + } + + test('Track backups (untitled)', function () { + return untitledBackupTest(); + }); + + test('Track backups (untitled with initial contents)', function () { + return untitledBackupTest({ contents: 'Foo Bar' }); + }); + + test('Track backups (custom)', async function () { + const { accessor, cleanup, backupFileService } = await createTracker(); + + class TestBackupWorkingCopy extends TestWorkingCopy { + + backupDelay = 0; + + constructor(resource: URI) { + super(resource); + + accessor.workingCopyService.registerWorkingCopy(this); + } + + async backup(token: CancellationToken): Promise { + await timeout(this.backupDelay); + + return {}; + } + } + + const resource = toResource.call(this, '/path/custom.txt'); + const customWorkingCopy = new TestBackupWorkingCopy(resource); + + // Normal + customWorkingCopy.setDirty(true); + await backupFileService.joinBackupResource(); + assert.strictEqual(backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + customWorkingCopy.setDirty(true); + await backupFileService.joinBackupResource(); + assert.strictEqual(backupFileService.hasBackupSync(resource), true); + + customWorkingCopy.setDirty(false); + await backupFileService.joinDiscardBackup(); + assert.strictEqual(backupFileService.hasBackupSync(resource), false); + + // Cancellation + customWorkingCopy.setDirty(true); + await timeout(0); + customWorkingCopy.setDirty(false); + await backupFileService.joinDiscardBackup(); + assert.strictEqual(backupFileService.hasBackupSync(resource), false); + + customWorkingCopy.dispose(); + cleanup(); + }); +}); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index 7c09bea8ef..fd20d86108 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; -import * as os from 'os'; -import * as path from 'vs/base/common/path'; -import * as pfs from 'vs/base/node/pfs'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { tmpdir } from 'os'; +import { promises } from 'fs'; +import { join } from 'vs/base/common/path'; +import { rimraf, writeFile } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hashPath } from 'vs/workbench/services/backup/electron-browser/backupFileService'; import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -17,18 +18,12 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -38,94 +33,96 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { registerTestFileEditor, TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { timeout } from 'vs/base/common/async'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { IProgressService } from 'vs/platform/progress/common/progress'; -const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); -const backupHome = path.join(userdataDir, 'Backups'); -const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); +flakySuite('BackupTracker (native)', function () { -const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); -const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); + class TestBackupTracker extends NativeBackupTracker { -class TestBackupTracker extends NativeBackupTracker { + constructor( + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILifecycleService lifecycleService: ILifecycleService, + @IFileDialogService fileDialogService: IFileDialogService, + @IDialogService dialogService: IDialogService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @INativeHostService nativeHostService: INativeHostService, + @ILogService logService: ILogService, + @IEditorService editorService: IEditorService, + @IEnvironmentService environmentService: IEnvironmentService, + @IProgressService progressService: IProgressService + ) { + super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService, progressService); + } - constructor( - @IBackupFileService backupFileService: IBackupFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @ILifecycleService lifecycleService: ILifecycleService, - @IFileDialogService fileDialogService: IFileDialogService, - @IDialogService dialogService: IDialogService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @INativeHostService nativeHostService: INativeHostService, - @ILogService logService: ILogService, - @IEditorService editorService: IEditorService, - @IEnvironmentService environmentService: IEnvironmentService - ) { - super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService); + protected getBackupScheduleDelay(): number { + return 10; // Reduce timeout for tests + } + + dispose() { + super.dispose(); + + for (const [_, disposable] of this.pendingBackups) { + disposable.dispose(); + } + } } - protected getBackupScheduleDelay(): number { - return 10; // Reduce timeout for tests + class BeforeShutdownEventImpl implements BeforeShutdownEvent { + + value: boolean | Promise | undefined; + reason = ShutdownReason.CLOSE; + + veto(value: boolean | Promise): void { + this.value = value; + } } -} -class BeforeShutdownEventImpl implements BeforeShutdownEvent { + let testDir: string; + let backupHome: string; + let workspaceBackupPath: string; - value: boolean | Promise | undefined; - reason = ShutdownReason.CLOSE; - - veto(value: boolean | Promise): void { - this.value = value; - } -} - -suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests let accessor: TestServiceAccessor; - let disposables: IDisposable[] = []; + const disposables = new DisposableStore(); + this.retries(3); + this.timeout(1000 * 20); setup(async () => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'backuprestorer'); + backupHome = join(testDir, 'Backups'); + const workspacesJsonPath = join(backupHome, 'workspaces.json'); + + const workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); + workspaceBackupPath = join(backupHome, hashPath(workspaceResource)); + const instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add((accessor.textFileService.files)); - disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TextFileEditor, - TextFileEditor.ID, - 'Text File Editor' - ), - [new SyncDescriptor(FileEditorInput)] - )); + disposables.add(registerTestFileEditor()); - // Delete any existing backups completely and then re-create it. - await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); - await pfs.mkdirp(backupHome); - await pfs.mkdirp(workspaceBackupPath); + await promises.mkdir(backupHome, { recursive: true }); + await promises.mkdir(workspaceBackupPath, { recursive: true }); - return pfs.writeFile(workspacesJsonPath, ''); + return writeFile(workspacesJsonPath, ''); }); teardown(async () => { - dispose(disposables); - disposables = []; + disposables.clear(); - (accessor.textFileService.files).dispose(); - - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + return rimraf(testDir); }); - async function createTracker(autoSaveEnabled = false): Promise<[TestServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { - const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { + const backupFileService = new NodeTestBackupFileService(testDir, workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -155,50 +152,19 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests const tracker = instantiationService.createInstance(TestBackupTracker); - return [accessor, part, tracker, instantiationService]; + const cleanup = async () => { + // File changes could also schedule some backup operations so we need to wait for them before finishing the test + await accessor.backupFileService.waitForAllBackups(); + + part.dispose(); + tracker.dispose(); + }; + + return { accessor, part, tracker, instantiationService, cleanup }; } - async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { - const [accessor, part, tracker] = await createTracker(); - - const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; - - const untitledModel = await untitledEditor.resolve(); - - if (!untitled?.contents) { - untitledModel.textEditorModel.setValue('Super Good'); - } - - await accessor.backupFileService.joinBackupResource(); - - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), true); - - untitledModel.dispose(); - - await accessor.backupFileService.joinDiscardBackup(); - - assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.resource), false); - - part.dispose(); - tracker.dispose(); - } - - test('Track backups (untitled)', function () { - this.timeout(20000); - - return untitledBackupTest(); - }); - - test('Track backups (untitled with initial contents)', function () { - this.timeout(20000); - - return untitledBackupTest({ contents: 'Foo Bar' }); - }); - - test.skip('Track backups (file)', async function () { // {{SQL CARBON EDIT}} tabcolorfailure - this.timeout(20000); - - const [accessor, part, tracker] = await createTracker(); + test('Track backups (file)', async function () { + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -208,69 +174,19 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); + assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), true); fileModel?.dispose(); await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); + assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), false); - part.dispose(); - tracker.dispose(); - }); - - test('Track backups (custom)', async function () { - const [accessor, part, tracker] = await createTracker(); - - class TestBackupWorkingCopy extends TestWorkingCopy { - - backupDelay = 0; - - constructor(resource: URI) { - super(resource); - - accessor.workingCopyService.registerWorkingCopy(this); - } - - async backup(token: CancellationToken): Promise { - await timeout(this.backupDelay); - - return {}; - } - } - - const resource = toResource.call(this, '/path/custom.txt'); - const customWorkingCopy = new TestBackupWorkingCopy(resource); - - // Normal - customWorkingCopy.setDirty(true); - await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); - - customWorkingCopy.setDirty(false); - customWorkingCopy.setDirty(true); - await accessor.backupFileService.joinBackupResource(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), true); - - customWorkingCopy.setDirty(false); - await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); - - // Cancellation - customWorkingCopy.setDirty(true); - await timeout(0); - customWorkingCopy.setDirty(false); - await accessor.backupFileService.joinDiscardBackup(); - assert.equal(accessor.backupFileService.hasBackupSync(resource), false); - - customWorkingCopy.dispose(); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto if no dirty files', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -281,12 +197,11 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests const veto = await event.value; assert.ok(!veto); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - veto if user cancels (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -298,7 +213,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -306,12 +221,11 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests const veto = await event.value; assert.ok(veto); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto if auto save is on', async function () { - const [accessor, part, tracker] = await createTracker(true /* auto save enabled */); + const { accessor, cleanup } = await createTracker(true /* auto save enabled */); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -320,7 +234,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -328,14 +242,13 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests const veto = await event.value; assert.ok(!veto); - assert.equal(accessor.workingCopyService.dirtyCount, 0); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -347,7 +260,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -355,12 +268,11 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests assert.ok(!veto); assert.ok(accessor.backupFileService.discardedBackups.length > 0); - part.dispose(); - tracker.dispose(); + await cleanup(); }); test('onWillShutdown - save (hot.exit: off)', async function () { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -372,7 +284,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); @@ -380,17 +292,16 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests assert.ok(!veto); assert.ok(!model?.isDirty()); - part.dispose(); - tracker.dispose(); + await cleanup(); }); suite('Hot Exit', () => { suite('"onExit" setting', () => { test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!platform.isMacintosh); + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!isMacintosh); }); test('should hot exit on non-Mac (reason: CLOSE, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!isMacintosh); }); test('should NOT hot exit (reason: CLOSE, windows: multiple, workspace)', function () { return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, true); @@ -441,7 +352,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, true, false); }); test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!isMacintosh); }); test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, true, false); @@ -488,7 +399,7 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests }); async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { - const [accessor, part, tracker] = await createTracker(); + const { accessor, cleanup } = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, options: { pinned: true } }); @@ -513,18 +424,17 @@ suite.skip('BackupTracker', () => { // {{SQL CARBON EDIT}} skip failing tests await model?.load(); model?.textEditorModel?.setValue('foo'); - assert.equal(accessor.workingCopyService.dirtyCount, 1); + assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); const event = new BeforeShutdownEventImpl(); event.reason = shutdownReason; accessor.lifecycleService.fireWillShutdown(event); const veto = await event.value; - assert.equal(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel - assert.equal(veto, shouldVeto); + assert.strictEqual(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.strictEqual(veto, shouldVeto); - part.dispose(); - tracker.dispose(); + await cleanup(); } }); }); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index 4929dae8cb..d3c03204cf 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -19,6 +19,8 @@ import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bu import { UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; class BulkEdit { @@ -30,6 +32,7 @@ class BulkEdit { private readonly _edits: ResourceEdit[], private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _confirmBeforeUndo: boolean, @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { @@ -76,7 +79,7 @@ class BulkEdit { } const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); + await this._performFileEdits(group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress); } else if (group[0] instanceof ResourceTextEdit) { await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress); } else if (group[0] instanceof ResourceNotebookCellEdit) { @@ -88,9 +91,9 @@ class BulkEdit { } } - private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, progress, this._token, edits); + const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); await model.apply(); } @@ -118,6 +121,8 @@ export class BulkEditService implements IBulkEditService { @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IEditorService private readonly _editorService: IEditorService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IDialogService private readonly _dialogService: IDialogService ) { } setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { @@ -175,18 +180,22 @@ export class BulkEditService implements IBulkEditService { undoRedoGroupRemove = this._activeUndoRedoGroups.push(undoRedoGroup); } + const label = options?.quotableLabel || options?.label; const bulkEdit = this._instaService.createInstance( BulkEdit, - options?.quotableLabel || options?.label, + label, codeEditor, options?.progress ?? Progress.None, options?.token ?? CancellationToken.None, edits, undoRedoGroup, - options?.undoRedoSource + options?.undoRedoSource, + !!options?.confirmBeforeUndo ); + let listener: IDisposable | undefined; try { + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label), 'veto.blukEditService')); await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { @@ -195,9 +204,20 @@ export class BulkEditService implements IBulkEditService { this._logService.error(err); throw err; } finally { + listener?.dispose(); undoRedoGroupRemove(); } } + + private async shouldVeto(label: string | undefined): Promise { + label = label || localize('fileOperation', "File operation"); + const result = await this._dialogService.confirm({ + message: localize('areYouSureQuiteBulkEdit', "Are you sure you want to quit? '{0}' is in progress.", label), + primaryButton: localize('quit', "Quit") + }); + + return !result.confirmed; + } } registerSingleton(IBulkEditService, BulkEditService, true); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 8080f9f1a4..b009a71ec1 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -8,20 +8,16 @@ import { WorkspaceFileEditOptions } from 'vs/editor/common/modes'; import { IFileService, FileSystemProviderCapabilities, IFileContent } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileService, IFileOperationUndoRedoInfo, IMoveOperation, ICopyOperation, IDeleteOperation, ICreateOperation, ICreateFileOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService, UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; -import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; - -interface IFileOperationUndoRedoInfo { - undoRedoGroupId?: number; - isUndoing?: boolean; -} +import { flatten, tail } from 'vs/base/common/arrays'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IFileOperation { uris: URI[]; @@ -36,114 +32,189 @@ class Noop implements IFileOperation { } } -class RenameOperation implements IFileOperation { - +class RenameEdit { + readonly type = 'rename'; constructor( readonly newUri: URI, readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, + readonly options: WorkspaceFileEditOptions + ) { } +} + +class RenameOperation implements IFileOperation { + + constructor( + private readonly _edits: RenameEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, ) { } get uris() { - return [this.newUri, this.oldUri]; + return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); } async perform(token: CancellationToken): Promise { - // rename - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + const moves: IMoveOperation[] = []; + const undoes: RenameEdit[] = []; + for (const edit of this._edits) { + // check: not overwriting, but ignoring, and the target file exists + const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri); + if (!skip) { + moves.push({ + file: { source: edit.oldUri, target: edit.newUri }, + overwrite: edit.options.overwrite + }); + + // reverse edit + undoes.push(new RenameEdit(edit.oldUri, edit.newUri, edit.options)); + } } - await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - return new RenameOperation(this.oldUri, this.newUri, this.options, { isUndoing: true }, this._workingCopyFileService, this._fileService); + if (moves.length === 0) { + return new Noop(); + } + + await this._workingCopyFileService.move(moves, this._undoRedoInfo, token); + return new RenameOperation(undoes, { isUndoing: true }, this._workingCopyFileService, this._fileService); } toString(): string { - const oldBasename = resources.basename(this.oldUri); - const newBasename = resources.basename(this.newUri); - if (oldBasename !== newBasename) { - return `(rename ${oldBasename} to ${newBasename})`; - } - return `(rename ${this.oldUri} to ${this.newUri})`; + return `(rename ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`; } } +class CopyEdit { + readonly type = 'copy'; + constructor( + readonly newUri: URI, + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions + ) { } +} + class CopyOperation implements IFileOperation { constructor( - readonly newUri: URI, - readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, + private readonly _edits: CopyEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, @IInstantiationService private readonly _instaService: IInstantiationService ) { } get uris() { - return [this.newUri, this.oldUri]; + return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); } async perform(token: CancellationToken): Promise { - // copy - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + // (1) create copy operations, remove noops + const copies: ICopyOperation[] = []; + for (const edit of this._edits) { + //check: not overwriting, but ignoring, and the target file exists + const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri); + if (!skip) { + copies.push({ file: { source: edit.oldUri, target: edit.newUri }, overwrite: edit.options.overwrite }); + } } - await this._workingCopyFileService.copy([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - return this._instaService.createInstance(DeleteOperation, this.newUri, this.options, { isUndoing: true }, true); + if (copies.length === 0) { + return new Noop(); + } + + // (2) perform the actual copy and use the return stats to build undo edits + const stats = await this._workingCopyFileService.copy(copies, this._undoRedoInfo, token); + const undoes: DeleteEdit[] = []; + + for (let i = 0; i < stats.length; i++) { + const stat = stats[i]; + const edit = this._edits[i]; + undoes.push(new DeleteEdit(stat.resource, { recursive: true, folder: this._edits[i].options.folder || stat.isDirectory, ...edit.options }, false)); + } + + return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } toString(): string { - return `(copy ${this.oldUri} to ${this.newUri})`; + return `(copy ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`; } } +class CreateEdit { + readonly type = 'create'; + constructor( + readonly newUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly contents: VSBuffer | undefined, + ) { } +} + class CreateOperation implements IFileOperation { constructor( - readonly newUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, - readonly contents: VSBuffer | undefined, + private readonly _edits: CreateEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IFileService private readonly _fileService: IFileService, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextFileService private readonly _textFileService: ITextFileService ) { } get uris() { - return [this.newUri]; + return this._edits.map(edit => edit.newUri); } async perform(token: CancellationToken): Promise { - // create file - if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) { - return new Noop(); // not overwriting, but ignoring, and the target file exists + + const folderCreates: ICreateOperation[] = []; + const fileCreates: ICreateFileOperation[] = []; + const undoes: DeleteEdit[] = []; + + for (const edit of this._edits) { + if (edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { + continue; // not overwriting, but ignoring, and the target file exists + } + if (edit.options.folder) { + folderCreates.push({ resource: edit.newUri }); + } else { + // If the contents are part of the edit they include the encoding, thus use them. Otherwise get the encoding for a new empty file. + const encodedReadable = typeof edit.contents !== 'undefined' ? edit.contents : await this._textFileService.getEncodedReadable(edit.newUri); + fileCreates.push({ resource: edit.newUri, contents: encodedReadable, overwrite: edit.options.overwrite }); + } + undoes.push(new DeleteEdit(edit.newUri, edit.options, !edit.options.folder && !edit.contents)); } - if (this.options.folder) { - await this._workingCopyFileService.createFolder(this.newUri, { ...this.undoRedoInfo }, token); - } else { - await this._workingCopyFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); + + if (folderCreates.length === 0 && fileCreates.length === 0) { + return new Noop(); } - return this._instaService.createInstance(DeleteOperation, this.newUri, this.options, { isUndoing: true }, !this.options.folder && !this.contents); + + await this._workingCopyFileService.createFolder(folderCreates, this._undoRedoInfo, token); + await this._workingCopyFileService.create(fileCreates, this._undoRedoInfo, token); + + return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } toString(): string { - return this.options.folder ? `create ${resources.basename(this.newUri)} folder` - : `(create ${resources.basename(this.newUri)} with ${this.contents?.byteLength || 0} bytes)`; + return `(create ${this._edits.map(edit => edit.options.folder ? `folder ${edit.newUri}` : `file ${edit.newUri} with ${edit.contents?.byteLength || 0} bytes`).join(', ')})`; } } +class DeleteEdit { + readonly type = 'delete'; + constructor( + readonly oldUri: URI, + readonly options: WorkspaceFileEditOptions, + readonly undoesCreate: boolean, + ) { } +} + class DeleteOperation implements IFileOperation { constructor( - readonly oldUri: URI, - readonly options: WorkspaceFileEditOptions, - readonly undoRedoInfo: IFileOperationUndoRedoInfo, - private readonly _undoesCreateOperation: boolean, + private _edits: DeleteEdit[], + private readonly _undoRedoInfo: IFileOperationUndoRedoInfo, @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IFileService private readonly _fileService: IFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -152,38 +223,58 @@ class DeleteOperation implements IFileOperation { ) { } get uris() { - return [this.oldUri]; + return this._edits.map(edit => edit.oldUri); } async perform(token: CancellationToken): Promise { // delete file - if (!await this._fileService.exists(this.oldUri)) { - if (!this.options.ignoreIfNotExists) { - throw new Error(`${this.oldUri} does not exist and can not be deleted`); - } - return new Noop(); - } - let fileContent: IFileContent | undefined; - if (!this._undoesCreateOperation && !this.options.folder) { - try { - fileContent = await this._fileService.readFile(this.oldUri); - } catch (err) { - this._logService.critical(err); + const deletes: IDeleteOperation[] = []; + const undoes: CreateEdit[] = []; + + for (const edit of this._edits) { + if (!await this._fileService.exists(edit.oldUri)) { + if (!edit.options.ignoreIfNotExists) { + throw new Error(`${edit.oldUri} does not exist and can not be deleted`); + } + continue; + } + + deletes.push({ + resource: edit.oldUri, + recursive: edit.options.recursive, + useTrash: !edit.options.skipTrashBin && this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash') + }); + + + // read file contents for undo operation. when a file is too large it won't be restored + let fileContent: IFileContent | undefined; + if (!edit.undoesCreate && !edit.options.folder) { + try { + fileContent = await this._fileService.readFile(edit.oldUri); + } catch (err) { + this._logService.critical(err); + } + } + if (!(typeof edit.options.maxSize === 'number' && fileContent && (fileContent?.size > edit.options.maxSize))) { + undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); } } - const useTrash = !this.options.skipTrashBin && this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash'); - await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive, ...this.undoRedoInfo }, token); - - if (typeof this.options.maxSize === 'number' && fileContent && (fileContent?.size > this.options.maxSize)) { + if (deletes.length === 0) { return new Noop(); } - return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, { isUndoing: true }, fileContent?.value); + + await this._workingCopyFileService.delete(deletes, this._undoRedoInfo, token); + + if (undoes.length === 0) { + return new Noop(); + } + return this._instaService.createInstance(CreateOperation, undoes, { isUndoing: true }); } toString(): string { - return `(delete ${resources.basename(this.oldUri)})`; + return `(delete ${this._edits.map(edit => edit.oldUri).join(', ')})`; } } @@ -195,7 +286,8 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { constructor( readonly label: string, - readonly operations: IFileOperation[] + readonly operations: IFileOperation[], + readonly confirmBeforeUndo: boolean ) { this.resources = ([]).concat(...operations.map(op => op.uris)); } @@ -216,7 +308,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { } } - public toString(): string { + toString(): string { return this.operations.map(op => String(op)).join(', '); } } @@ -227,6 +319,7 @@ export class BulkFileEdits { private readonly _label: string, private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _confirmBeforeUndo: boolean, private readonly _progress: IProgress, private readonly _token: CancellationToken, private readonly _edits: ResourceFileEdit[], @@ -237,34 +330,66 @@ export class BulkFileEdits { async apply(): Promise { const undoOperations: IFileOperation[] = []; const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id }; + + const edits: Array = []; for (const edit of this._edits) { + if (edit.newResource && edit.oldResource && !edit.options?.copy) { + edits.push(new RenameEdit(edit.newResource, edit.oldResource, edit.options ?? {})); + } else if (edit.newResource && edit.oldResource && edit.options?.copy) { + edits.push(new CopyEdit(edit.newResource, edit.oldResource, edit.options ?? {})); + } else if (!edit.newResource && edit.oldResource) { + edits.push(new DeleteEdit(edit.oldResource, edit.options ?? {}, false)); + } else if (edit.newResource && !edit.oldResource) { + edits.push(new CreateEdit(edit.newResource, edit.options ?? {}, undefined)); + } + } + + if (edits.length === 0) { + return; + } + + const groups: Array[] = []; + groups[0] = [edits[0]]; + + for (let i = 1; i < edits.length; i++) { + const edit = edits[i]; + const lastGroup = tail(groups); + if (lastGroup[0].type === edit.type) { + lastGroup.push(edit); + } else { + groups.push([edit]); + } + } + + for (let group of groups) { if (this._token.isCancellationRequested) { break; } - const options = edit.options || {}; let op: IFileOperation | undefined; - if (edit.newResource && edit.oldResource && !options.copy) { - // rename - op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options, undoRedoInfo); - } else if (edit.newResource && edit.oldResource && options.copy) { - op = this._instaService.createInstance(CopyOperation, edit.newResource, edit.oldResource, options, undoRedoInfo); - } else if (!edit.newResource && edit.oldResource) { - // delete file - op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options, undoRedoInfo, false); - } else if (edit.newResource && !edit.oldResource) { - // create file - op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undoRedoInfo, undefined); + switch (group[0].type) { + case 'rename': + op = this._instaService.createInstance(RenameOperation, group, undoRedoInfo); + break; + case 'copy': + op = this._instaService.createInstance(CopyOperation, group, undoRedoInfo); + break; + case 'delete': + op = this._instaService.createInstance(DeleteOperation, group, undoRedoInfo); + break; + case 'create': + op = this._instaService.createInstance(CreateOperation, group, undoRedoInfo); + break; } + if (op) { const undoOp = await op.perform(this._token); undoOperations.push(undoOp); } - this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations), this._undoRedoGroup, this._undoRedoSource); + this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index 16d476475e..6204c14b0a 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -346,7 +346,7 @@ const refactorPreviewViewIcon = registerIcon('refactor-preview-view-icon', Codic const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: BulkEditPane.ID, - name: localize('panel', "Refactor Preview"), + title: localize('panel', "Refactor Preview"), hideIfEmpty: true, ctorDescriptor: new SyncDescriptor( ViewPaneContainer, diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 5cbe45caf4..bd0d1b2353 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -17,7 +17,7 @@ import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } fr import { ILabelService } from 'vs/platform/label/common/label'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { URI } from 'vs/base/common/uri'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -298,7 +298,7 @@ export class BulkEditPane extends ViewPane { } } - private async _openElementAsEditor(e: IOpenEvent): Promise { + private async _openElementAsEditor(e: IOpenEvent): Promise { type Mutable = { -readonly [P in keyof T]: T[P] }; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index 25551a7943..30f6708370 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -409,14 +409,14 @@ export class CategoryElementRenderer implements ITreeRenderer pfs.symlink(getSource(), target)) + .then(() => fs.promises.symlink(getSource(), target)) .then(undefined, err => { if (err.code === 'EACCES' || err.code === 'ENOENT') { return new Promise((resolve, reject) => { @@ -111,7 +112,7 @@ class InstallAction extends Action2 { } private isInstalled(target: string): Promise { - return pfs.lstat(target) + return fs.promises.lstat(target) .then(stat => stat.isSymbolicLink()) .then(() => extpath.realpath(target)) .then(link => link === getSource()) @@ -149,7 +150,7 @@ class UninstallAction extends Action2 { } const uninstall = () => { - return pfs.unlink(target) + return fs.promises.unlink(target) .then(undefined, ignore('ENOENT', null)); }; diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 002758990d..b1634c7496 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -37,7 +37,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey('accessibilityHelpWidgetVisible', false); -class AccessibilityHelpController extends Disposable implements IEditorContribution { +export class AccessibilityHelpController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.accessibilityHelpController'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 7cbf86578d..c95305106e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -8,10 +8,10 @@ import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; const enum WidgetState { @@ -48,7 +48,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont Severity.Warning, nls.localize('hintTimeout', "The diff algorithm was stopped early (after {0} ms.)", this._diffEditor.maxComputationTime), [{ - label: nls.localize('removeTimeout', "Remove limit"), + label: nls.localize('removeTimeout', "Remove Limit"), run: () => { this._configurationService.updateValue('diffEditor.maxComputationTime', 0); } @@ -94,7 +94,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont private _onDidClickHelperWidget(): void { if (this._state === WidgetState.HintWhitespace) { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false, ConfigurationTarget.USER); + this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index 7559db5610..b8b915f254 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { CharacterPair, CommentRule, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration'; +import { CharacterPair, CommentRule, EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -31,6 +31,19 @@ interface IIndentationRules { unIndentedLinePattern?: string | IRegExp; } +interface IEnterAction { + indent: 'none' | 'indent' | 'indentOutdent' | 'outdent'; + appendText?: string; + removeText?: number; +} + +interface IOnEnterRule { + beforeText: string | IRegExp; + afterText?: string | IRegExp; + previousLineText?: string | IRegExp; + action: IEnterAction; +} + interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; @@ -40,6 +53,7 @@ interface ILanguageConfiguration { indentationRules?: IIndentationRules; folding?: FoldingRules; autoCloseBefore?: string; + onEnterRules?: IOnEnterRule[]; } function isStringArr(something: string[] | null): something is string[] { @@ -93,7 +107,7 @@ export class LanguageConfigurationFileHandler { } this._done[languageIdentifier.id] = true; - let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); + const configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language); configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation)); } @@ -254,18 +268,82 @@ export class LanguageConfigurationFileHandler { return result; } - // private _mapCharacterPairs(pairs: Array): IAutoClosingPairConditional[] { - // return pairs.map(pair => { - // if (Array.isArray(pair)) { - // return { open: pair[0], close: pair[1] }; - // } - // return pair; - // }); - // } + private _extractValidOnEnterRules(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): OnEnterRule[] | null { + const source = configuration.onEnterRules; + if (typeof source === 'undefined') { + return null; + } + if (!Array.isArray(source)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules\` to be an array.`); + return null; + } + + let result: OnEnterRule[] | null = null; + for (let i = 0, len = source.length; i < len; i++) { + const onEnterRule = source[i]; + if (!types.isObject(onEnterRule)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}]\` to be an object.`); + continue; + } + if (!types.isObject(onEnterRule.action)) { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action\` to be an object.`); + continue; + } + let indentAction: IndentAction; + if (onEnterRule.action.indent === 'none') { + indentAction = IndentAction.None; + } else if (onEnterRule.action.indent === 'indent') { + indentAction = IndentAction.Indent; + } else if (onEnterRule.action.indent === 'indentOutdent') { + indentAction = IndentAction.IndentOutdent; + } else if (onEnterRule.action.indent === 'outdent') { + indentAction = IndentAction.Outdent; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.indent\` to be 'none', 'indent', 'indentOutdent' or 'outdent'.`); + continue; + } + const action: EnterAction = { indentAction }; + if (onEnterRule.action.appendText) { + if (typeof onEnterRule.action.appendText === 'string') { + action.appendText = onEnterRule.action.appendText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.appendText\` to be undefined or a string.`); + } + } + if (onEnterRule.action.removeText) { + if (typeof onEnterRule.action.removeText === 'number') { + action.removeText = onEnterRule.action.removeText; + } else { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.removeText\` to be undefined or a number.`); + } + } + const beforeText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].beforeText`, onEnterRule.beforeText); + if (!beforeText) { + continue; + } + const resultingOnEnterRule: OnEnterRule = { beforeText, action }; + if (onEnterRule.afterText) { + const afterText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].afterText`, onEnterRule.afterText); + if (afterText) { + resultingOnEnterRule.afterText = afterText; + } + } + if (onEnterRule.previousLineText) { + const previousLineText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].previousLineText`, onEnterRule.previousLineText); + if (previousLineText) { + resultingOnEnterRule.previousLineText = previousLineText; + } + } + result = result || []; + result.push(resultingOnEnterRule); + } + + return result; + } private _handleConfig(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): void { - let richEditConfig: LanguageConfiguration = {}; + const richEditConfig: LanguageConfiguration = {}; const comments = this._extractValidCommentRule(languageIdentifier, configuration); if (comments) { @@ -293,25 +371,21 @@ export class LanguageConfigurationFileHandler { } if (configuration.wordPattern) { - try { - let wordPattern = this._parseRegex(configuration.wordPattern); - if (wordPattern) { - richEditConfig.wordPattern = wordPattern; - } - } catch (error) { - // Malformed regexes are ignored + const wordPattern = this._parseRegex(languageIdentifier, `wordPattern`, configuration.wordPattern); + if (wordPattern) { + richEditConfig.wordPattern = wordPattern; } } if (configuration.indentationRules) { - let indentationRules = this._mapIndentationRules(configuration.indentationRules); + const indentationRules = this._mapIndentationRules(languageIdentifier, configuration.indentationRules); if (indentationRules) { richEditConfig.indentationRules = indentationRules; } } if (configuration.folding) { - let markers = configuration.folding.markers; + const markers = configuration.folding.markers; richEditConfig.folding = { offSide: configuration.folding.offSide, @@ -319,44 +393,66 @@ export class LanguageConfigurationFileHandler { }; } - LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig); + const onEnterRules = this._extractValidOnEnterRules(languageIdentifier, configuration); + if (onEnterRules) { + richEditConfig.onEnterRules = onEnterRules; + } + + LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig, 50); } - private _parseRegex(value: string | IRegExp) { + private _parseRegex(languageIdentifier: LanguageIdentifier, confPath: string, value: string | IRegExp) { if (typeof value === 'string') { - return new RegExp(value, ''); - } else if (typeof value === 'object') { - return new RegExp(value.pattern, value.flags); + try { + return new RegExp(value, ''); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } } - + if (types.isObject(value)) { + if (typeof value.pattern !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.pattern\` to be a string.`); + return null; + } + if (typeof value.flags !== 'undefined' && typeof value.flags !== 'string') { + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.flags\` to be a string.`); + return null; + } + try { + return new RegExp(value.pattern, value.flags); + } catch (err) { + console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err); + return null; + } + } + console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}\` to be a string or an object.`); return null; } - private _mapIndentationRules(indentationRules: IIndentationRules): IndentationRule | null { - try { - let increaseIndentPattern = this._parseRegex(indentationRules.increaseIndentPattern); - let decreaseIndentPattern = this._parseRegex(indentationRules.decreaseIndentPattern); - - if (increaseIndentPattern && decreaseIndentPattern) { - let result: IndentationRule = { - increaseIndentPattern: increaseIndentPattern, - decreaseIndentPattern: decreaseIndentPattern - }; - - if (indentationRules.indentNextLinePattern) { - result.indentNextLinePattern = this._parseRegex(indentationRules.indentNextLinePattern); - } - if (indentationRules.unIndentedLinePattern) { - result.unIndentedLinePattern = this._parseRegex(indentationRules.unIndentedLinePattern); - } - - return result; - } - } catch (error) { - // Malformed regexes are ignored + private _mapIndentationRules(languageIdentifier: LanguageIdentifier, indentationRules: IIndentationRules): IndentationRule | null { + const increaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern); + if (!increaseIndentPattern) { + return null; + } + const decreaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.decreaseIndentPattern`, indentationRules.decreaseIndentPattern); + if (!decreaseIndentPattern) { + return null; } - return null; + const result: IndentationRule = { + increaseIndentPattern: increaseIndentPattern, + decreaseIndentPattern: decreaseIndentPattern + }; + + if (indentationRules.indentNextLinePattern) { + result.indentNextLinePattern = this._parseRegex(languageIdentifier, `indentationRules.indentNextLinePattern`, indentationRules.indentNextLinePattern); + } + if (indentationRules.unIndentedLinePattern) { + result.unIndentedLinePattern = this._parseRegex(languageIdentifier, `indentationRules.unIndentedLinePattern`, indentationRules.unIndentedLinePattern); + } + + return result; } } @@ -601,6 +697,101 @@ const schema: IJSONSchema = { } } } + }, + onEnterRules: { + type: 'array', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + items: { + type: 'object', + description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'), + required: ['beforeText', 'action'], + properties: { + beforeText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.beforeText', 'This rule will only execute if the text before the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.pattern', 'The RegExp pattern for beforeText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.beforeText.flags', 'The RegExp flags for beforeText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.beforeText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + afterText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.afterText', 'This rule will only execute if the text after the cursor matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.pattern', 'The RegExp pattern for afterText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.afterText.flags', 'The RegExp flags for afterText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.afterText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + previousLineText: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.previousLineText', 'This rule will only execute if the text above the line matches this regular expression.'), + properties: { + pattern: { + type: 'string', + description: nls.localize('schema.onEnterRules.previousLineText.pattern', 'The RegExp pattern for previousLineText.'), + default: '', + }, + flags: { + type: 'string', + description: nls.localize('schema.onEnterRules.previousLineText.flags', 'The RegExp flags for previousLineText.'), + default: '', + pattern: '^([gimuy]+)$', + patternErrorMessage: nls.localize('schema.onEnterRules.previousLineText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.') + } + } + }, + action: { + type: ['string', 'object'], + description: nls.localize('schema.onEnterRules.action', 'The action to execute.'), + required: ['indent'], + default: { 'indent': 'indent' }, + properties: { + indent: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.indent', "Describe what to do with the indentation"), + default: 'indent', + enum: ['none', 'indent', 'indentOutdent', 'outdent'], + markdownEnumDescriptions: [ + nls.localize('schema.onEnterRules.action.indent.none', "Insert new line and copy the previous line's indentation."), + nls.localize('schema.onEnterRules.action.indent.indent', "Insert new line and indent once (relative to the previous line's indentation)."), + nls.localize('schema.onEnterRules.action.indent.indentOutdent', "Insert two new lines:\n - the first one indented which will hold the cursor\n - the second one at the same indentation level"), + nls.localize('schema.onEnterRules.action.indent.outdent', "Insert new line and outdent once (relative to the previous line's indentation).") + ] + }, + appendText: { + type: 'string', + description: nls.localize('schema.onEnterRules.action.appendText', 'Describes text to be appended after the new line and after the indentation.'), + default: '', + }, + removeText: { + type: 'number', + description: nls.localize('schema.onEnterRules.action.removeText', 'Describes the number of characters to remove from the new line\'s indentation.'), + default: 0, + } + } + } + } + } } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts index fa700aa548..c1b2ad83af 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts @@ -46,7 +46,7 @@ export class LargeFileOptimizationsWarner extends Disposable implements IEditorC this._notificationService.prompt(Severity.Info, message, [ { - label: nls.localize('removeOptimizations', "Forcefully enable features"), + label: nls.localize('removeOptimizations', "Forcefully Enable Features"), run: () => { this._configurationService.updateValue(`editor.largeFileOptimizations`, false).then(() => { this._notificationService.info(nls.localize('reopenFilePrompt', "Please reopen file in order for this setting to take effect.")); diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts new file mode 100644 index 0000000000..5de584e7e8 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -0,0 +1,429 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IBreadcrumbsDataSource, IOutline, IOutlineCreator, IOutlineListConfig, IOutlineService, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget, } from 'vs/workbench/services/outline/browser/outline'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate } from 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { raceCancellation, TimeoutTimer, timeout, Barrier } from 'vs/base/common/async'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPosition } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorOptions, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { IDataSource } from 'vs/base/browser/ui/tree/tree'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { localize } from 'vs/nls'; +import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { isEqual } from 'vs/base/common/resources'; + +type DocumentSymbolItem = OutlineGroup | OutlineElement; + +class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource{ + + private _breadcrumbs: (OutlineGroup | OutlineElement)[] = []; + + constructor( + private readonly _editor: ICodeEditor, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, + ) { } + + getBreadcrumbElements(): readonly DocumentSymbolItem[] { + return this._breadcrumbs; + } + + clear(): void { + this._breadcrumbs = []; + } + + update(model: OutlineModel, position: IPosition): void { + const newElements = this._computeBreadcrumbs(model, position); + this._breadcrumbs = newElements; + } + + private _computeBreadcrumbs(model: OutlineModel, position: IPosition): Array { + let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position); + if (!item) { + return []; + } + let chain: Array = []; + while (item) { + chain.push(item); + let parent: any = item.parent; + if (parent instanceof OutlineModel) { + break; + } + if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) { + break; + } + item = parent; + } + let result: Array = []; + for (let i = chain.length - 1; i >= 0; i--) { + let element = chain[i]; + if (this._isFiltered(element)) { + break; + } + result.push(element); + } + if (result.length === 0) { + return []; + } + return result; + } + + private _isFiltered(element: TreeElement): boolean { + if (!(element instanceof OutlineElement)) { + return false; + } + const key = `breadcrumbs.${DocumentSymbolFilter.kindToConfigName[element.symbol.kind]}`; + let uri: URI | undefined; + if (this._editor && this._editor.getModel()) { + const model = this._editor.getModel() as ITextModel; + uri = model.uri; + } + return !this._textResourceConfigurationService.getValue(uri, key); + } +} + +class DocumentSymbolsOutline implements IOutline { + + private readonly _disposables = new DisposableStore(); + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _outlineModel?: OutlineModel; + private _outlineDisposables = new DisposableStore(); + + private readonly _breadcrumbsDataSource: DocumentSymbolBreadcrumbsSource; + + readonly config: IOutlineListConfig; + + readonly outlineKind = 'documentSymbols'; + + get activeElement(): DocumentSymbolItem | undefined { + const posistion = this._editor.getPosition(); + if (!posistion || !this._outlineModel) { + return undefined; + } else { + return this._outlineModel.getItemEnclosingPosition(posistion); + } + } + + constructor( + private readonly _editor: ICodeEditor, + target: OutlineTarget, + firstLoadBarrier: Barrier, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + + this._breadcrumbsDataSource = new DocumentSymbolBreadcrumbsSource(_editor, textResourceConfigurationService); + const delegate = new DocumentSymbolVirtualDelegate(); + const renderers = [new DocumentSymbolGroupRenderer(), instantiationService.createInstance(DocumentSymbolRenderer, true)]; + const treeDataSource: IDataSource = { + getChildren: (parent) => { + if (parent instanceof OutlineElement || parent instanceof OutlineGroup) { + return parent.children.values(); + } + if (parent === this && this._outlineModel) { + return this._outlineModel.children.values(); + } + return []; + } + }; + const comparator = new DocumentSymbolComparator(); + const options = { + collapseByDefault: target === OutlineTarget.Breadcrumbs, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + identityProvider: new DocumentSymbolIdentityProvider(), + keyboardNavigationLabelProvider: new DocumentSymbolNavigationLabelProvider(), + accessibilityProvider: new DocumentSymbolAccessibilityProvider(localize('document', "Document Symbols")), + filter: target === OutlineTarget.OutlinePane + ? instantiationService.createInstance(DocumentSymbolFilter, 'outline') + : target === OutlineTarget.Breadcrumbs + ? instantiationService.createInstance(DocumentSymbolFilter, 'breadcrumbs') + : undefined + }; + + this.config = { + breadcrumbsDataSource: this._breadcrumbsDataSource, + delegate, + renderers, + treeDataSource, + comparator, + options, + quickPickDataSource: { getQuickPickElements: () => { throw new Error('not implemented'); } } + }; + + + // update as language, model, providers changes + this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._createOutline())); + this._disposables.add(this._editor.onDidChangeModel(_ => this._createOutline())); + this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._createOutline())); + + // update soon'ish as model content change + const updateSoon = new TimeoutTimer(); + this._disposables.add(updateSoon); + this._disposables.add(this._editor.onDidChangeModelContent(event => { + const timeout = OutlineModel.getRequestDelay(this._editor!.getModel()); + updateSoon.cancelAndSet(() => this._createOutline(event), timeout); + })); + + // stop when editor dies + this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear())); + + // initial load + this._createOutline().finally(() => firstLoadBarrier.open()); + } + + dispose(): void { + this._disposables.dispose(); + this._outlineDisposables.dispose(); + } + + get isEmpty(): boolean { + return !this._outlineModel || TreeElement.empty(this._outlineModel); + } + + async reveal(entry: DocumentSymbolItem, options: IEditorOptions, sideBySide: boolean): Promise { + const model = OutlineModel.get(entry); + if (!model || !(entry instanceof OutlineElement)) { + return; + } + await this._codeEditorService.openCodeEditor({ + resource: model.uri, + options: { + ...options, + selection: Range.collapseToStart(entry.symbol.selectionRange), + selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport, + } + }, this._editor, sideBySide); + } + + preview(entry: DocumentSymbolItem): IDisposable { + if (!(entry instanceof OutlineElement)) { + return Disposable.None; + } + + const { symbol } = entry; + this._editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); + const ids = this._editor.deltaDecorations([], [{ + range: symbol.range, + options: { + className: 'rangeHighlight', + isWholeLine: true + } + }]); + return toDisposable(() => this._editor.deltaDecorations(ids, [])); + } + + captureViewState(): IDisposable { + const viewState = this._editor.saveViewState(); + return toDisposable(() => { + if (viewState) { + this._editor.restoreViewState(viewState); + } + }); + } + + private async _createOutline(contentChangeEvent?: IModelContentChangedEvent): Promise { + + this._outlineDisposables.clear(); + if (!contentChangeEvent) { + this._setOutlineModel(undefined); + } + + if (!this._editor.hasModel()) { + return; + } + const buffer = this._editor.getModel(); + if (!DocumentSymbolProviderRegistry.has(buffer)) { + return; + } + + const cts = new CancellationTokenSource(); + const versionIdThen = buffer.getVersionId(); + const timeoutTimer = new TimeoutTimer(); + + this._outlineDisposables.add(timeoutTimer); + this._outlineDisposables.add(toDisposable(() => cts.dispose(true))); + + try { + let model = await OutlineModel.create(buffer, cts.token); + if (cts.token.isCancellationRequested) { + // cancelled -> do nothing + return; + } + + if (TreeElement.empty(model) || !this._editor.hasModel()) { + // empty -> no outline elements + this._setOutlineModel(model); + return; + } + + // heuristic: when the symbols-to-lines ratio changes by 50% between edits + // wait a little (and hope that the next change isn't as drastic). + if (contentChangeEvent && this._outlineModel && buffer.getLineCount() >= 25) { + const newSize = TreeElement.size(model); + const newLength = buffer.getValueLength(); + const newRatio = newSize / newLength; + const oldSize = TreeElement.size(this._outlineModel); + const oldLength = newLength - contentChangeEvent.changes.reduce((prev, value) => prev + value.rangeLength, 0); + const oldRatio = oldSize / oldLength; + if (newRatio <= oldRatio * 0.5 || newRatio >= oldRatio * 1.5) { + // wait for a better state and ignore current model when more + // typing has happened + const value = await raceCancellation(timeout(2000).then(() => true), cts.token, false); + if (!value) { + return; + } + } + } + + // copy the model + model = model.adopt(); + + // feature: show markers with outline element + this._applyMarkersToOutline(model); + this._outlineDisposables.add(this._markerDecorationsService.onDidChangeMarker(textModel => { + if (isEqual(model.uri, textModel.uri)) { + this._applyMarkersToOutline(model); + this._onDidChange.fire({}); + } + })); + this._outlineDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + this._applyMarkersToOutline(model); + } else { + model.updateMarker([]); + } + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('outline')) { + // outline filtering, problems on/off + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('breadcrumbs') && this._editor.hasModel()) { + // breadcrumbs filtering + this._breadcrumbsDataSource.update(model, this._editor.getPosition()); + this._onDidChange.fire({}); + } + })); + + // feature: toggle icons + this._outlineDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.icons)) { + this._onDidChange.fire({}); + } + if (e.affectsConfiguration('outline')) { + this._onDidChange.fire({}); + } + })); + + // feature: update active when cursor changes + this._outlineDisposables.add(this._editor.onDidChangeCursorPosition(_ => { + timeoutTimer.cancelAndSet(() => { + if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && this._editor.hasModel()) { + this._breadcrumbsDataSource.update(model, this._editor.getPosition()); + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + }, 150); + })); + + // update properties, send event + this._setOutlineModel(model); + + } catch (err) { + this._setOutlineModel(undefined); + onUnexpectedError(err); + } + } + + private _applyMarkersToOutline(model: OutlineModel | undefined): void { + if (!model || !this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + return; + } + const markers: IOutlineMarker[] = []; + for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(model.uri)) { + if (marker.severity === MarkerSeverity.Error || marker.severity === MarkerSeverity.Warning) { + markers.push({ ...range, severity: marker.severity }); + } + } + model.updateMarker(markers); + } + + private _setOutlineModel(model: OutlineModel | undefined) { + const position = this._editor.getPosition(); + if (!position || !model) { + this._outlineModel = undefined; + this._breadcrumbsDataSource.clear(); + } else { + if (!this._outlineModel?.merge(model)) { + this._outlineModel = model; + } + this._breadcrumbsDataSource.update(model, position); + } + this._onDidChange.fire({}); + } +} + +class DocumentSymbolsOutlineCreator implements IOutlineCreator { + + readonly dispose: () => void; + + constructor( + @IOutlineService outlineService: IOutlineService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + const reg = outlineService.registerOutlineCreator(this); + this.dispose = () => reg.dispose(); + } + + matches(candidate: IEditorPane): candidate is IEditorPane { + const ctrl = candidate.getControl(); + return isCodeEditor(ctrl) || isDiffEditor(ctrl); + } + + async createOutline(pane: IEditorPane, target: OutlineTarget, _token: CancellationToken): Promise | undefined> { + const control = pane.getControl(); + let editor: ICodeEditor | undefined; + if (isCodeEditor(control)) { + editor = control; + } else if (isDiffEditor(control)) { + editor = control.getModifiedEditor(); + } + if (!editor) { + return undefined; + } + const firstLoadBarrier = new Barrier(); + const result = this._instantiationService.createInstance(DocumentSymbolsOutline, editor, target, firstLoadBarrier); + await firstLoadBarrier.wait(); + return result; + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DocumentSymbolsOutlineCreator, LifecyclePhase.Eventually); diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css similarity index 77% rename from src/vs/editor/contrib/documentSymbols/media/outlineTree.css rename to src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css index b22f9d8d5b..26d7c9b10c 100644 --- a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css @@ -20,11 +20,7 @@ color: var(--outline-element-color); } -.monaco-list .outline-element .monaco-icon-label-container .monaco-highlighted-label, -.monaco-list .outline-element .monaco-icon-label-container .label-description { - white-space: nowrap; -} - +.monaco-breadcrumbs .outline-element .outline-element-decoration, .monaco-list .outline-element .outline-element-decoration { opacity: 0.75; font-size: 90%; @@ -35,10 +31,17 @@ color: var(--outline-element-color); } +/* when showing in breadcrumbs than hide a few things, like markers or descriptions */ +.monaco-breadcrumbs .outline-element .monaco-icon-label-container .monaco-icon-description-container, +.monaco-breadcrumbs .outline-element .outline-element-decoration { + display: none; +} + .monaco-list .outline-element .outline-element-decoration.bubble { font-family: codicon; font-size: 14px; opacity: 0.4; + padding-right: 8px; } .monaco-list .outline-element .outline-element-icon { diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts new file mode 100644 index 0000000000..c9eabb1938 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -0,0 +1,305 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./documentSymbolsTree'; +import 'vs/editor/contrib/symbolIcons/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors +import * as dom from 'vs/base/browser/dom'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeNode, ITreeRenderer, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { Range } from 'vs/editor/common/core/range'; +import { SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; +import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { localize } from 'vs/nls'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IdleValue } from 'vs/base/common/async'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IOutlineComparator, OutlineConfigKeys } from 'vs/workbench/services/outline/browser/outline'; + +export type DocumentSymbolItem = OutlineGroup | OutlineElement; + +export class DocumentSymbolNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + + getKeyboardNavigationLabel(element: DocumentSymbolItem): { toString(): string; } { + if (element instanceof OutlineGroup) { + return element.label; + } else { + return element.symbol.name; + } + } +} + +export class DocumentSymbolAccessibilityProvider implements IListAccessibilityProvider { + + constructor(private readonly _ariaLabel: string) { } + + getWidgetAriaLabel(): string { + return this._ariaLabel; + } + getAriaLabel(element: DocumentSymbolItem): string | null { + if (element instanceof OutlineGroup) { + return element.label; + } else { + return element.symbol.name; + } + } +} + +export class DocumentSymbolIdentityProvider implements IIdentityProvider { + getId(element: DocumentSymbolItem): { toString(): string; } { + return element.id; + } +} + +class DocumentSymbolGroupTemplate { + static readonly id = 'DocumentSymbolGroupTemplate'; + constructor( + readonly labelContainer: HTMLElement, + readonly label: HighlightedLabel, + ) { } +} + +class DocumentSymbolTemplate { + static readonly id = 'DocumentSymbolTemplate'; + constructor( + readonly container: HTMLElement, + readonly iconLabel: IconLabel, + readonly iconClass: HTMLElement, + readonly decoration: HTMLElement, + ) { } +} + +export class DocumentSymbolVirtualDelegate implements IListVirtualDelegate { + + getHeight(_element: DocumentSymbolItem): number { + return 22; + } + + getTemplateId(element: DocumentSymbolItem): string { + return element instanceof OutlineGroup + ? DocumentSymbolGroupTemplate.id + : DocumentSymbolTemplate.id; + } +} + +export class DocumentSymbolGroupRenderer implements ITreeRenderer { + + readonly templateId: string = DocumentSymbolGroupTemplate.id; + + renderTemplate(container: HTMLElement): DocumentSymbolGroupTemplate { + const labelContainer = dom.$('.outline-element-label'); + container.classList.add('outline-element'); + dom.append(container, labelContainer); + return new DocumentSymbolGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true)); + } + + renderElement(node: ITreeNode, _index: number, template: DocumentSymbolGroupTemplate): void { + template.label.set(node.element.label, createMatches(node.filterData)); + } + + disposeTemplate(_template: DocumentSymbolGroupTemplate): void { + // nothing + } +} + +export class DocumentSymbolRenderer implements ITreeRenderer { + + readonly templateId: string = DocumentSymbolTemplate.id; + + constructor( + private _renderMarker: boolean, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IThemeService private readonly _themeService: IThemeService, + ) { } + + renderTemplate(container: HTMLElement): DocumentSymbolTemplate { + container.classList.add('outline-element'); + const iconLabel = new IconLabel(container, { supportHighlights: true }); + const iconClass = dom.$('.outline-element-icon'); + const decoration = dom.$('.outline-element-decoration'); + container.prepend(iconClass); + container.appendChild(decoration); + return new DocumentSymbolTemplate(container, iconLabel, iconClass, decoration); + } + + renderElement(node: ITreeNode, _index: number, template: DocumentSymbolTemplate): void { + const { element } = node; + const options: IIconLabelValueOptions = { + matches: createMatches(node.filterData), + labelEscapeNewLines: true, + extraClasses: ['nowrap'], + title: localize('title.template', "{0} ({1})", element.symbol.name, DocumentSymbolRenderer._symbolKindNames[element.symbol.kind]) + }; + if (this._configurationService.getValue(OutlineConfigKeys.icons)) { + // add styles for the icons + template.iconClass.className = ''; + template.iconClass.classList.add(`outline-element-icon`, ...SymbolKinds.toCssClassName(element.symbol.kind, true).split(' ')); + } + if (element.symbol.tags.indexOf(SymbolTag.Deprecated) >= 0) { + options.extraClasses!.push(`deprecated`); + options.matches = []; + } + template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options); + + if (this._renderMarker) { + this._renderMarkerInfo(element, template); + } + } + + private _renderMarkerInfo(element: OutlineElement, template: DocumentSymbolTemplate): void { + + if (!element.marker) { + dom.hide(template.decoration); + template.container.style.removeProperty('--outline-element-color'); + return; + } + + const { count, topSev } = element.marker; + const color = this._themeService.getColorTheme().getColor(topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground); + const cssColor = color ? color.toString() : 'inherit'; + + // color of the label + if (this._configurationService.getValue(OutlineConfigKeys.problemsColors)) { + template.container.style.setProperty('--outline-element-color', cssColor); + } else { + template.container.style.removeProperty('--outline-element-color'); + } + + // badge with color/rollup + if (!this._configurationService.getValue(OutlineConfigKeys.problemsBadges)) { + dom.hide(template.decoration); + + } else if (count > 0) { + dom.show(template.decoration); + template.decoration.classList.remove('bubble'); + template.decoration.innerText = count < 10 ? count.toString() : '+9'; + template.decoration.title = count === 1 ? localize('1.problem', "1 problem in this element") : localize('N.problem', "{0} problems in this element", count); + template.decoration.style.setProperty('--outline-element-color', cssColor); + + } else { + dom.show(template.decoration); + template.decoration.classList.add('bubble'); + template.decoration.innerText = '\uea71'; + template.decoration.title = localize('deep.problem', "Contains elements with problems"); + template.decoration.style.setProperty('--outline-element-color', cssColor); + } + } + + private static _symbolKindNames: { [symbol: number]: string } = { + [SymbolKind.Array]: localize('Array', "array"), + [SymbolKind.Boolean]: localize('Boolean', "boolean"), + [SymbolKind.Class]: localize('Class', "class"), + [SymbolKind.Constant]: localize('Constant', "constant"), + [SymbolKind.Constructor]: localize('Constructor', "constructor"), + [SymbolKind.Enum]: localize('Enum', "enumeration"), + [SymbolKind.EnumMember]: localize('EnumMember', "enumeration member"), + [SymbolKind.Event]: localize('Event', "event"), + [SymbolKind.Field]: localize('Field', "field"), + [SymbolKind.File]: localize('File', "file"), + [SymbolKind.Function]: localize('Function', "function"), + [SymbolKind.Interface]: localize('Interface', "interface"), + [SymbolKind.Key]: localize('Key', "key"), + [SymbolKind.Method]: localize('Method', "method"), + [SymbolKind.Module]: localize('Module', "module"), + [SymbolKind.Namespace]: localize('Namespace', "namespace"), + [SymbolKind.Null]: localize('Null', "null"), + [SymbolKind.Number]: localize('Number', "number"), + [SymbolKind.Object]: localize('Object', "object"), + [SymbolKind.Operator]: localize('Operator', "operator"), + [SymbolKind.Package]: localize('Package', "package"), + [SymbolKind.Property]: localize('Property', "property"), + [SymbolKind.String]: localize('String', "string"), + [SymbolKind.Struct]: localize('Struct', "struct"), + [SymbolKind.TypeParameter]: localize('TypeParameter', "type parameter"), + [SymbolKind.Variable]: localize('Variable', "variable"), + }; + + disposeTemplate(_template: DocumentSymbolTemplate): void { + _template.iconLabel.dispose(); + } +} + +export class DocumentSymbolFilter implements ITreeFilter { + + static readonly kindToConfigName = Object.freeze({ + [SymbolKind.File]: 'showFiles', + [SymbolKind.Module]: 'showModules', + [SymbolKind.Namespace]: 'showNamespaces', + [SymbolKind.Package]: 'showPackages', + [SymbolKind.Class]: 'showClasses', + [SymbolKind.Method]: 'showMethods', + [SymbolKind.Property]: 'showProperties', + [SymbolKind.Field]: 'showFields', + [SymbolKind.Constructor]: 'showConstructors', + [SymbolKind.Enum]: 'showEnums', + [SymbolKind.Interface]: 'showInterfaces', + [SymbolKind.Function]: 'showFunctions', + [SymbolKind.Variable]: 'showVariables', + [SymbolKind.Constant]: 'showConstants', + [SymbolKind.String]: 'showStrings', + [SymbolKind.Number]: 'showNumbers', + [SymbolKind.Boolean]: 'showBooleans', + [SymbolKind.Array]: 'showArrays', + [SymbolKind.Object]: 'showObjects', + [SymbolKind.Key]: 'showKeys', + [SymbolKind.Null]: 'showNull', + [SymbolKind.EnumMember]: 'showEnumMembers', + [SymbolKind.Struct]: 'showStructs', + [SymbolKind.Event]: 'showEvents', + [SymbolKind.Operator]: 'showOperators', + [SymbolKind.TypeParameter]: 'showTypeParameters', + }); + + constructor( + private readonly _prefix: 'breadcrumbs' | 'outline', + @ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService, + ) { } + + filter(element: DocumentSymbolItem): boolean { + const outline = OutlineModel.get(element); + if (!(element instanceof OutlineElement)) { + return true; + } + const configName = DocumentSymbolFilter.kindToConfigName[element.symbol.kind]; + const configKey = `${this._prefix}.${configName}`; + return this._textResourceConfigService.getValue(outline?.uri, configKey); + } +} + +export class DocumentSymbolComparator implements IOutlineComparator { + + private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); + + compareByPosition(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); + } + return 0; + } + compareByType(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); + } + return 0; + } + compareByName(a: DocumentSymbolItem, b: DocumentSymbolItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.order - b.order; + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + } + return 0; + } +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index bd41aa7084..2e495796e8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -30,10 +30,10 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv } private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; + const editorConfig = this.configurationService.getValue().workbench?.editor; return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openEditorPinned: !editorConfig?.enablePreviewFromQuickOpen || !editorConfig?.enablePreview }; } @@ -44,12 +44,12 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv protected gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void { // Check for sideBySide use - if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned, + pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus }, SIDE_GROUP); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index fb239c0570..35a0b269b1 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -12,9 +12,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; -import { DisposableStore, IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -23,10 +23,11 @@ import { prepareQuery } from 'vs/base/common/fuzzyScorer'; import { SymbolKind } from 'vs/editor/common/modes'; import { fuzzyScore, createMatches } from 'vs/base/common/filters'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { isCompositeEditor } from 'vs/editor/browser/editorBrowser'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -34,7 +35,8 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess constructor( @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IOutlineService private readonly outlineService: IOutlineService, ) { super({ openSideBySideDirection: () => this.configuration.openSideBySideDirection @@ -43,36 +45,37 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#region DocumentSymbols (text editor required) - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); - } - - return super.provideWithTextEditor(context, picker, token); - } - private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; + const editorConfig = this.configurationService.getValue().workbench?.editor; return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection + openEditorPinned: !editorConfig?.enablePreviewFromQuickOpen || !editorConfig?.enablePreview, + openSideBySideDirection: editorConfig?.openSideBySideDirection }; } protected get activeTextEditorControl() { + // TODO@bpasero this distinction should go away by adopting `IOutlineService` + // for all editors (either text based ones or not). Currently text based + // editors are not yet using the new outline service infrastructure but the + // "classical" document symbols approach. + + if (isCompositeEditor(this.editorService.activeEditorPane?.getControl())) { + return undefined; + } + return this.editorService.activeTextEditorControl; } protected gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void { // Check for sideBySide use - if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.alt || (this.configuration.openEditorPinned && options.keyMods.ctrlCmd) || options.forceSideBySide) && this.editorService.activeEditor) { context.restoreViewState?.(); // since we open to the side, restore view state in this editor this.editorService.openEditor(this.editorService.activeEditor, { selection: options.range, - pinned: options.keyMods.alt || this.configuration.openEditorPinned, + pinned: options.keyMods.ctrlCmd || this.configuration.openEditorPinned, preserveFocus: options.preserveFocus }, SIDE_GROUP); } @@ -104,7 +107,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return []; } - return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), options, token); + return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token); } addDecorations(editor: IEditor, range: IRange): void { @@ -118,22 +121,21 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#endregion protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); + if (this.canPickWithOutlineService()) { + return this.doGetOutlinePicks(picker); } return super.provideWithoutTextEditor(picker); } - private canPickFromTableOfContents(): boolean { - return this.editorService.activeEditorPane ? TableOfContentsProviderRegistry.has(this.editorService.activeEditorPane.getId()) : false; + private canPickWithOutlineService(): boolean { + return this.editorService.activeEditorPane ? this.outlineService.canCreateOutline(this.editorService.activeEditorPane) : false; } - private doGetTableOfContentsPicks(picker: IQuickPick): IDisposable { + private doGetOutlinePicks(picker: IQuickPick): IDisposable { const pane = this.editorService.activeEditorPane; if (!pane) { return Disposable.None; } - const provider = TableOfContentsProviderRegistry.get(pane.getId())!; const cts = new CancellationTokenSource(); const disposables = new DisposableStore(); @@ -141,30 +143,45 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess picker.busy = true; - provider.provideTableOfContents(pane, { disposables }, cts.token).then(entries => { + this.outlineService.createOutline(pane, OutlineTarget.QuickPick, cts.token).then(outline => { - picker.busy = false; - - if (cts.token.isCancellationRequested || !entries || entries.length === 0) { + if (!outline) { return; } + if (cts.token.isCancellationRequested) { + outline.dispose(); + return; + } + + disposables.add(outline); + + const viewState = outline.captureViewState(); + disposables.add(toDisposable(() => { + if (picker.selectedItems.length === 0) { + viewState.dispose(); + } + })); + + const entries = Array.from(outline.config.quickPickDataSource.getQuickPickElements()); const items: IGotoSymbolQuickPickItem[] = entries.map((entry, idx) => { return { kind: SymbolKind.File, index: idx, score: 0, - label: entry.icon ? `$(${entry.icon.id}) ${entry.label}` : entry.label, - ariaLabel: entry.detail ? `${entry.label}, ${entry.detail}` : entry.label, - detail: entry.detail, + label: entry.label, description: entry.description, + ariaLabel: entry.ariaLabel, + iconClasses: entry.iconClasses }; }); disposables.add(picker.onDidAccept(() => { picker.hide(); const [entry] = picker.selectedItems; - entries[entry.index]?.pick(); + if (entry && entries[entry.index]) { + outline.reveal(entries[entry.index].element, {}, false); + } })); const updatePickerItems = () => { @@ -194,16 +211,23 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess updatePickerItems(); disposables.add(picker.onDidChangeValue(updatePickerItems)); + const previewDisposable = new MutableDisposable(); + disposables.add(previewDisposable); + disposables.add(picker.onDidChangeActive(() => { const [entry] = picker.activeItems; - if (entry) { - entries[entry.index]?.preview(); + if (entry && entries[entry.index]) { + previewDisposable.value = outline.preview(entries[entry.index].element); + } else { + previewDisposable.clear(); } })); }).catch(err => { onUnexpectedError(err); picker.hide(); + }).finally(() => { + picker.busy = false; }); return disposables; @@ -243,45 +267,3 @@ registerAction2(class GotoSymbolAction extends Action2 { accessor.get(IQuickInputService).quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); } }); - -//#region toc definition and logic - -export interface ITableOfContentsEntry { - icon?: ThemeIcon; - label: string; - detail?: string; - description?: string; - pick(): any; - preview(): any; -} - -export interface ITableOfContentsProvider { - - provideTableOfContents(editor: T, context: { disposables: DisposableStore }, token: CancellationToken): Promise; -} - -class ProviderRegistry { - - private readonly _provider = new Map(); - - register(type: string, provider: ITableOfContentsProvider): IDisposable { - this._provider.set(type, provider); - return toDisposable(() => { - if (this._provider.get(type) === provider) { - this._provider.delete(type); - } - }); - } - - get(type: string): ITableOfContentsProvider | undefined { - return this._provider.get(type); - } - - has(type: string): boolean { - return this._provider.has(type); - } -} - -export const TableOfContentsProviderRegistry = new ProviderRegistry(); - -//#endregion diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index b2e6943a11..7c5ff7771a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -10,10 +10,9 @@ import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EditorOption, EditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -29,14 +28,7 @@ const isDominatedByLongLinesKey = 'isDominatedByLongLines'; * State written/read by the toggle word wrap action and associated with a particular model. */ interface IWordWrapTransientState { - readonly forceWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded'; - readonly forceWordWrapMinified: boolean; -} - -interface IWordWrapState { - readonly configuredWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded' | undefined; - readonly configuredWordWrapMinified: boolean; - readonly transientState: IWordWrapTransientState | null; + readonly wordWrapOverride: 'on' | 'off'; } /** @@ -49,70 +41,10 @@ export function writeTransientState(model: ITextModel, state: IWordWrapTransient /** * Read (in memory) the word wrap state for a particular model. */ -function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState { +function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState | null { return codeEditorService.getTransientModelProperty(model, transientWordWrapState); } -function readWordWrapState(model: ITextModel, configurationService: ITextResourceConfigurationService, codeEditorService: ICodeEditorService): IWordWrapState { - const editorConfig = configurationService.getValue(model.uri, 'editor') as { wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded'; wordWrapMinified: boolean }; - let _configuredWordWrap = editorConfig && (typeof editorConfig.wordWrap === 'string' || typeof editorConfig.wordWrap === 'boolean') ? editorConfig.wordWrap : undefined; - - // Compatibility with old true or false values - if (_configuredWordWrap === true) { - _configuredWordWrap = 'on'; - } else if (_configuredWordWrap === false) { - _configuredWordWrap = 'off'; - } - - const _configuredWordWrapMinified = editorConfig && typeof editorConfig.wordWrapMinified === 'boolean' ? editorConfig.wordWrapMinified : undefined; - const _transientState = readTransientState(model, codeEditorService); - return { - configuredWordWrap: _configuredWordWrap, - configuredWordWrapMinified: (typeof _configuredWordWrapMinified === 'boolean' ? _configuredWordWrapMinified : EditorOptions.wordWrapMinified.defaultValue), - transientState: _transientState - }; -} - -function toggleWordWrap(editor: ICodeEditor, state: IWordWrapState): IWordWrapState { - if (state.transientState) { - // toggle off => go to null - return { - configuredWordWrap: state.configuredWordWrap, - configuredWordWrapMinified: state.configuredWordWrapMinified, - transientState: null - }; - } - - let transientState: IWordWrapTransientState; - - const actualWrappingInfo = editor.getOption(EditorOption.wrappingInfo); - if (actualWrappingInfo.isWordWrapMinified) { - // => wrapping due to minified file - transientState = { - forceWordWrap: 'off', - forceWordWrapMinified: false - }; - } else if (state.configuredWordWrap !== 'off') { - // => wrapping is configured to be on (or some variant) - transientState = { - forceWordWrap: 'off', - forceWordWrapMinified: false - }; - } else { - // => wrapping is configured to be off - transientState = { - forceWordWrap: 'on', - forceWordWrapMinified: state.configuredWordWrapMinified - }; - } - - return { - configuredWordWrap: state.configuredWordWrap, - configuredWordWrapMinified: state.configuredWordWrapMinified, - transientState: transientState - }; -} - const TOGGLE_WORD_WRAP_ID = 'editor.action.toggleWordWrap'; class ToggleWordWrapAction extends EditorAction { @@ -139,7 +71,6 @@ class ToggleWordWrapAction extends EditorAction { return; } - const textResourceConfigurationService = accessor.get(ITextResourceConfigurationService); const codeEditorService = accessor.get(ICodeEditorService); const model = editor.getModel(); @@ -148,12 +79,21 @@ class ToggleWordWrapAction extends EditorAction { } // Read the current state - const currentState = readWordWrapState(model, textResourceConfigurationService, codeEditorService); + const transientState = readTransientState(model, codeEditorService); + // Compute the new state - const newState = toggleWordWrap(editor, currentState); + let newState: IWordWrapTransientState | null; + if (transientState) { + newState = null; + } else { + const actualWrappingInfo = editor.getOption(EditorOption.wrappingInfo); + const wordWrapOverride = (actualWrappingInfo.wrappingColumn === -1 ? 'on' : 'off'); + newState = { wordWrapOverride }; + } + // Write the new state // (this will cause an event and the controller will apply the state) - writeTransientState(model, newState.transientState, codeEditorService); + writeTransientState(model, newState, codeEditorService); } } @@ -162,24 +102,23 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution public static readonly ID = 'editor.contrib.toggleWordWrapController'; constructor( - private readonly editor: ICodeEditor, - @IContextKeyService readonly contextKeyService: IContextKeyService, - @ITextResourceConfigurationService readonly configurationService: ITextResourceConfigurationService, - @ICodeEditorService readonly codeEditorService: ICodeEditorService + private readonly _editor: ICodeEditor, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(); - const options = this.editor.getOptions(); + const options = this._editor.getOptions(); const wrappingInfo = options.get(EditorOption.wrappingInfo); - const isWordWrapMinified = this.contextKeyService.createKey(isWordWrapMinifiedKey, wrappingInfo.isWordWrapMinified); - const isDominatedByLongLines = this.contextKeyService.createKey(isDominatedByLongLinesKey, wrappingInfo.isDominatedByLongLines); + const isWordWrapMinified = this._contextKeyService.createKey(isWordWrapMinifiedKey, wrappingInfo.isWordWrapMinified); + const isDominatedByLongLines = this._contextKeyService.createKey(isDominatedByLongLinesKey, wrappingInfo.isDominatedByLongLines); let currentlyApplyingEditorConfig = false; - this._register(editor.onDidChangeConfiguration((e) => { + this._register(_editor.onDidChangeConfiguration((e) => { if (!e.hasChanged(EditorOption.wrappingInfo)) { return; } - const options = this.editor.getOptions(); + const options = this._editor.getOptions(); const wrappingInfo = options.get(EditorOption.wrappingInfo); isWordWrapMinified.set(wrappingInfo.isWordWrapMinified); isDominatedByLongLines.set(wrappingInfo.isDominatedByLongLines); @@ -189,25 +128,25 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution } })); - this._register(editor.onDidChangeModel((e) => { + this._register(_editor.onDidChangeModel((e) => { ensureWordWrapSettings(); })); - this._register(codeEditorService.onDidChangeTransientModelProperty(() => { + this._register(_codeEditorService.onDidChangeTransientModelProperty(() => { ensureWordWrapSettings(); })); const ensureWordWrapSettings = () => { - if (this.editor.getContribution(DefaultSettingsEditorContribution.ID)) { + if (this._editor.getContribution(DefaultSettingsEditorContribution.ID)) { // in the settings editor... return; } - if (this.editor.isSimpleWidget) { + if (this._editor.isSimpleWidget) { // in a simple widget... return; } // Ensure correct word wrap settings - const newModel = this.editor.getModel(); + const newModel = this._editor.getModel(); if (!newModel) { return; } @@ -216,33 +155,22 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution return; } - // Read current configured values and toggle state - const desiredState = readWordWrapState(newModel, this.configurationService, this.codeEditorService); + const transientState = readTransientState(newModel, this._codeEditorService); // Apply the state try { currentlyApplyingEditorConfig = true; - this._applyWordWrapState(desiredState); + this._applyWordWrapState(transientState); } finally { currentlyApplyingEditorConfig = false; } }; } - private _applyWordWrapState(state: IWordWrapState): void { - if (state.transientState) { - // toggle is on - this.editor.updateOptions({ - wordWrap: state.transientState.forceWordWrap, - wordWrapMinified: state.transientState.forceWordWrapMinified - }); - return; - } - - // toggle is off - this.editor.updateOptions({ - wordWrap: state.configuredWordWrap, - wordWrapMinified: state.configuredWordWrapMinified + private _applyWordWrapState(state: IWordWrapTransientState | null): void { + const wordWrapOverride2 = state ? state.wordWrapOverride : 'inherit'; + this._editor.updateOptions({ + wordWrapOverride2: wordWrapOverride2 }); } } diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index d1d5fbf20e..255003526a 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -42,25 +42,25 @@ suite('Save Participants', function () { let lineContent = ''; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // No new line if last line already empty lineContent = `Hello New Line${model.textEditorModel.getEOL()}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // New empty line added (single line) lineContent = 'Hello New Line'; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); // New empty line added (multi line) lineContent = `Hello New Line${model.textEditorModel.getEOL()}Hello New Line${model.textEditorModel.getEOL()}Hello New Line`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${lineContent}${model.textEditorModel.getEOL()}`); }); test('trim final new lines', async function () { @@ -77,25 +77,25 @@ suite('Save Participants', function () { let lineContent = `${textContent}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // No new line removal if last line is single new line lineContent = `${textContent}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), lineContent); + assert.strictEqual(snapshotToString(model.createSnapshot()!), lineContent); // Remove new line (single line with two new lines) lineContent = `${textContent}${eol}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); // Remove new lines (multiple lines with multiple new lines) lineContent = `${textContent}${eol}${textContent}${eol}${eol}${eol}`; model.textEditorModel.setValue(lineContent); await participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${textContent}${eol}`); }); test('trim final new lines bug#39750', async function () { @@ -117,12 +117,12 @@ suite('Save Participants', function () { // undo await model.textEditorModel.undo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); // trim final new lines should not mess the undo stack await participant.participate(model, { reason: SaveReason.EXPLICIT }); await model.textEditorModel.redo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}.`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}.`); }); test('trim final new lines bug#46075', async function () { @@ -143,13 +143,13 @@ suite('Save Participants', function () { } // confirm trimming - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); // undo should go back to previous content immediately await model.textEditorModel.undo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}${eol}`); await model.textEditorModel.redo(); - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); }); test('trim whitespace', async function () { @@ -169,6 +169,6 @@ suite('Save Participants', function () { } // confirm trimming - assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + assert.strictEqual(snapshotToString(model.createSnapshot()!), `${textContent}`); }); }); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 59ea1f4eff..4683b8db16 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -25,7 +25,7 @@ import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { peekViewBorder } from 'vs/editor/contrib/peekView/peekView'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -45,7 +45,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; -import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; @@ -239,15 +238,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); this._actionbarWidget = new ActionBar(actionsContainer, { - actionViewItemProvider: (action: IAction) => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } else { - return new ActionViewItem({}, action, { label: false, icon: true }); - } - } + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) }); this._disposables.add(this._actionbarWidget); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 0defcbaf1b..a6f32d0257 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -142,6 +142,7 @@ export class CommentNodeRenderer implements IListRenderer } templateData.commentText.appendChild(renderedComment); + templateData.commentText.title = renderedComment.textContent ?? ''; } disposeTemplate(templateData: ICommentThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index beb2b47e5c..940859faad 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -6,11 +6,9 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { basename, isEqual } from 'vs/base/common/resources'; -import { IAction, Action } from 'vs/base/common/actions'; -import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; +import { basename } from 'vs/base/common/resources'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; @@ -20,14 +18,19 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentsList, COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyEqualsExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; + +const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); export class CommentsPanel extends ViewPane { private treeLabels!: ResourceLabels; @@ -35,7 +38,7 @@ export class CommentsPanel extends ViewPane { private treeContainer!: HTMLElement; private messageBoxContainer!: HTMLElement; private commentsModel!: CommentsModel; - private collapseAllAction?: IAction; + private readonly hasCommentsContextKey: IContextKey; readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; @@ -52,8 +55,10 @@ export class CommentsPanel extends ViewPane { @IThemeService themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.hasCommentsContextKey = CONTEXT_KEY_HAS_COMMENTS.bindTo(contextKeyService); } public renderBody(container: HTMLElement): void { @@ -129,13 +134,14 @@ export class CommentsPanel extends ViewPane { await this.tree.setInput(this.commentsModel); } - public getActions(): IAction[] { - if (!this.collapseAllAction) { - this.collapseAllAction = new Action('vs.tree.collapse', nls.localize('collapseAll', "Collapse All"), 'collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); - this._register(this.collapseAllAction); + public collapseAll() { + if (this.tree) { + this.tree.collapseAll(); + this.tree.setSelection([]); + this.tree.setFocus([]); + this.tree.domFocus(); + this.tree.focusFirst(); } - - return [this.collapseAllAction]; } public layoutBody(height: number, width: number): void { @@ -206,7 +212,7 @@ export class CommentsPanel extends ViewPane { const activeEditor = this.editorService.activeEditor; let currentActiveResource = activeEditor ? activeEditor.resource : undefined; - if (currentActiveResource && isEqual(currentActiveResource, element.resource)) { + if (this.uriIdentityService.extUri.isEqual(element.resource, currentActiveResource)) { const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; const control = this.editorService.activeTextEditorControl; @@ -243,9 +249,7 @@ export class CommentsPanel extends ViewPane { private async refresh(): Promise { if (this.isVisible()) { - if (this.collapseAllAction) { - this.collapseAllAction.enabled = this.commentsModel.hasCommentThreads(); - } + this.hasCommentsContextKey.set(this.commentsModel.hasCommentThreads()); this.treeContainer.classList.toggle('hidden', !this.commentsModel.hasCommentThreads()); this.renderMessage(); @@ -281,3 +285,23 @@ CommandsRegistry.registerCommand({ viewsService.openView(COMMENTS_VIEW_ID, true); } }); + +registerAction2(class Collapse extends ViewAction { + constructor() { + super({ + viewId: COMMENTS_VIEW_ID, + id: 'comments.collapse', + title: nls.localize('collapseAll', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('view', COMMENTS_VIEW_ID), CONTEXT_KEY_HAS_COMMENTS]) + } + }); + } + runInView(_accessor: ServicesAccessor, view: CommentsPanel) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index baaa39a46c..fb6dd09cd7 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -397,7 +397,7 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before { - content: "💬"; + content: "+"; } .monaco-editor .comment-range-glyph.comment-thread { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts index 2dbef918b9..53562d0c81 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts @@ -6,14 +6,14 @@ import { Schemas } from 'vs/base/common/network'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { CustomEditorInputFactory } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; +import { customEditorInputFactory, CustomEditorInputFactory } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewEditor } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditor'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { CustomEditorInput } from './customEditorInput'; import { CustomEditorContribution, CustomEditorService } from './customEditors'; @@ -38,4 +38,4 @@ Registry.as(EditorInputExtensions.EditorInputFactor CustomEditorInputFactory); Registry.as(EditorInputExtensions.EditorInputFactories) - .registerCustomEditorInputFactory(Schemas.vscodeCustomEditor, CustomEditorInputFactory); + .registerCustomEditorInputFactory(Schemas.vscodeCustomEditor, customEditorInputFactory); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 5a4e43bad2..7ab9b34a09 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -20,7 +20,6 @@ import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/c import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @@ -47,7 +46,6 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @ILabelService private readonly labelService: ILabelService, @ICustomEditorService private readonly customEditorService: ICustomEditorService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IEditorService private readonly editorService: IEditorService, @IUndoRedoService private readonly undoRedoService: IUndoRedoService, ) { @@ -118,22 +116,6 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return this._modelRef.object.isDirty(); } - public isSaving(): boolean { - if (this.isUntitled()) { - return false; // untitled is never saving automatically - } - - if (!this.isDirty()) { - return false; // the editor needs to be dirty for being saved - } - - if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return true; // a short auto save is configured, treat this as being saved - } - - return false; - } - public async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { if (!this._modelRef) { return undefined; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index d911e2ed1b..0c43227073 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -6,7 +6,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInput } from 'vs/workbench/common/editor'; +import { ICustomEditorInputFactory, IEditorInput } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { IWebviewService, WebviewExtensionDescription, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { reviveWebviewExtensionDescription, SerializedWebview, WebviewEditorInputFactory, DeserializedWebview } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInputFactory'; @@ -85,27 +85,29 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { serializedEditorInput: string ): CustomEditorInput { const data = this.fromJson(JSON.parse(serializedEditorInput)); - const webview = CustomEditorInputFactory.reviveWebview(data, this._webviewService); + const webview = reviveWebview(data, this._webviewService); const customInput = this._instantiationService.createInstance(CustomEditorInput, data.editorResource, data.viewType, data.id, webview, { startsDirty: data.dirty, backupId: data.backupId }); if (typeof data.group === 'number') { customInput.updateGroup(data.group); } return customInput; } +} - private static reviveWebview(data: { id: string, state: any, options: WebviewInputOptions, extension?: WebviewExtensionDescription, }, webviewService: IWebviewService) { - return new Lazy(() => { - const webview = webviewService.createWebviewOverlay(data.id, { - purpose: WebviewContentPurpose.CustomEditor, - enableFindWidget: data.options.enableFindWidget, - retainContextWhenHidden: data.options.retainContextWhenHidden - }, data.options, data.extension); - webview.state = data.state; - return webview; - }); - } +function reviveWebview(data: { id: string, state: any, options: WebviewInputOptions, extension?: WebviewExtensionDescription, }, webviewService: IWebviewService) { + return new Lazy(() => { + const webview = webviewService.createWebviewOverlay(data.id, { + purpose: WebviewContentPurpose.CustomEditor, + enableFindWidget: data.options.enableFindWidget, + retainContextWhenHidden: data.options.retainContextWhenHidden + }, data.options, data.extension); + webview.state = data.state; + return webview; + }); +} - public static createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { +export const customEditorInputFactory = new class implements ICustomEditorInputFactory { + public createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { return instantiationService.invokeFunction(async accessor => { const webviewService = accessor.get(IWebviewService); const backupFileService = accessor.get(IBackupFileService); @@ -118,7 +120,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { const backupData = backup.meta; const id = backupData.webview.id; const extension = reviveWebviewExtensionDescription(backupData.extension?.id, backupData.extension?.location); - const webview = CustomEditorInputFactory.reviveWebview({ id, options: backupData.webview.options, state: backupData.webview.state, extension, }, webviewService); + const webview = reviveWebview({ id, options: backupData.webview.options, state: backupData.webview.state, extension, }, webviewService); const editor = instantiationService.createInstance(CustomEditorInput, URI.revive(backupData.editorResource), backupData.viewType, id, webview, { backupId: backupData.backupId }); editor.updateGroup(0); @@ -126,7 +128,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { }); } - public static canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { + public canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { if (editorInput instanceof CustomEditorInput) { if (editorInput.resource.path === backupResource.path && backupResource.authority === editorInput.viewType) { return true; @@ -135,4 +137,4 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { return false; } -} +}; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index d962930868..ca4c5d5d47 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct } from 'vs/base/common/arrays'; +import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; @@ -28,7 +28,7 @@ import { EditorInput, EditorOptions, Extensions as EditorInputExtensions, GroupI import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CONTEXT_CUSTOM_EDITORS, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; -import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CustomEditorAssociation, CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService, IOpenEditorOverride, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; @@ -45,7 +45,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _customEditorContextKey: IContextKey; private readonly _focusedCustomEditorIsEditable: IContextKey; - private readonly _webviewHasOwnEditFunctions: IContextKey; private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; @@ -66,7 +65,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._customEditorContextKey = CONTEXT_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); - this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); this._contributedEditors = this._register(new ContributedCustomEditors(storageService)); this._register(this._contributedEditors.onChange(() => { @@ -156,13 +154,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ...this.getAllCustomEditors(resource).allEditors, ]); - let currentlyOpenedEditorType: undefined | string; - for (const editor of group ? group.editors : []) { - if (editor.resource && isEqual(editor.resource, resource)) { - currentlyOpenedEditorType = editor instanceof CustomEditorInput ? editor.viewType : defaultCustomEditor.id; - break; - } - } + const existingEditorForResource = group && firstOrDefault(this.editorService.findEditors(resource, group)); + const currentlyOpenedEditorType: undefined | string = existingEditorForResource instanceof CustomEditorInput ? existingEditorForResource.viewType : defaultCustomEditor.id; const resourceExt = extname(resource); @@ -278,9 +271,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } // Try to replace existing editors for resource - const existingEditors = targetGroup.editors.filter(editor => editor.resource && isEqual(editor.resource, resource)); - if (existingEditors.length) { - const existing = existingEditors[0]; + const existing = firstOrDefault(this.editorService.findEditors(resource, targetGroup)); + if (existing) { if (!input.matches(existing)) { await this.editorService.replaceEditors([{ editor: existing, @@ -317,7 +309,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!resource) { this._customEditorContextKey.reset(); this._focusedCustomEditorIsEditable.reset(); - this._webviewHasOwnEditFunctions.reset(); return; } @@ -325,7 +316,6 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._customEditorContextKey.set(possibleEditors.map(x => x.id).join(',')); this._focusedCustomEditorIsEditable.set(activeEditorPane?.input instanceof CustomEditorInput); - this._webviewHasOwnEditFunctions.set(possibleEditors.length > 0); } private async handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): Promise { @@ -457,7 +447,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return this.onEditorOpening(editor, options, group); }, getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[] => { - const currentEditor = group?.editors.find(editor => isEqual(editor.resource, resource)); + const currentEditor = group && firstOrDefault(this.editorService.findEditors(resource, group)); const toOverride = (entry: CustomEditorInfo): IOpenEditorOverrideEntry => { return { @@ -546,7 +536,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo return undefined; // {{SQL CARBON EDIT}} Strict-null-checks } - const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.resource)); + const existingEditorForResource = firstOrDefault(this.editorService.findEditors(resource, group)); if (existingEditorForResource) { if (editor === existingEditorForResource) { return undefined; // {{SQL CARBON EDIT}} strict-null-checks diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 616cec46db..cf7b50f3c1 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -15,7 +15,6 @@ import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { RemoveBreakpointAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -173,6 +172,24 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi this.setDecorationsScheduler.schedule(); } + /** + * Returns context menu actions at the line number if breakpoints can be + * set. This is used by the {@link TestingDecorations} to allow breakpoint + * setting on lines where breakpoint "run" actions are present. + */ + public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) { + if (!this.debugService.getAdapterManager().hasDebuggers()) { + return []; + } + + if (!this.debugService.canSetBreakpointsIn(model)) { + return []; + } + + const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri }); + return this.getContextMenuActions(breakpoints, model.uri, lineNumber); + } + private registerListeners(): void { this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => { if (!this.debugService.getAdapterManager().hasDebuggers()) { @@ -301,7 +318,9 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const actions: IAction[] = []; if (breakpoints.length === 1) { const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); + actions.push(new Action('debug.removeBreakpoint', nls.localize('removeBreakpoint', "Remove {0}", breakpointType), undefined, true, async () => { + await this.debugService.removeBreakpoints(breakpoints[0].getId()); + })); actions.push(new Action( 'workbench.debug.action.editBreakpointAction', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), @@ -375,7 +394,8 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const decorations = this.editor.getLineDecorations(line); if (decorations) { for (const { options } of decorations) { - if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('codicon-') === -1) { + const clz = options.glyphMarginClassName; + if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) { return false; } } @@ -454,7 +474,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there // In practice this happens for the first breakpoint that was set on a line // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information - const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.debugBreakpointDisabled; + const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.breakpoint.disabled; const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); @@ -645,15 +665,11 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground); if (debugIconBreakpointColor) { collector.addRule(` - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointConditional)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointLog)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointFunction)}, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointData)}, + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']), - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, - .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugBreakpoint)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after, + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after { color: ${debugIconBreakpointColor} !important; } `); @@ -662,7 +678,7 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); if (debugIconBreakpointDisabledColor) { collector.addRule(` - .monaco-workbench .codicon[class*='-disabled'] { + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.disabled)}`).join(',\n ')} { color: ${debugIconBreakpointDisabledColor} !important; } `); @@ -671,7 +687,7 @@ registerThemingParticipant((theme, collector) => { const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground); if (debugIconBreakpointUnverifiedColor) { collector.addRule(` - .monaco-workbench .codicon[class*='-unverified'] { + ${icons.allBreakpoints.map(b => `.monaco-workbench ${ThemeIcon.asCSSSelector(b.unverified)}`).join(',\n ')} { color: ${debugIconBreakpointUnverifiedColor}; } `); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 9ca1f22f1c..8fa56361fc 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -221,6 +221,9 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.input = scopedInstatiationService.createInstance(CodeEditorWidget, container, options, codeEditorWidgetOptions); CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true); const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true); + if (this.editor.hasModel()) { + model.setMode(this.editor.getModel().getLanguageIdentifier()); + } this.input.setModel(model); this.toDispose.push(model); const setDecorations = () => { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 4d5a7ae6b6..0458e1a3dc 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -3,13 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugModel, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { IAction } from 'vs/base/common/actions'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugModel, IDataBreakpoint, BREAKPOINTS_VIEW_ID, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, IBaseBreakpoint, IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_INPUT_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; -import { AddFunctionBreakpointAction, ToggleBreakpointsActivatedAction, RemoveAllBreakpointsAction, RemoveBreakpointAction, EnableAllBreakpointsAction, DisableAllBreakpointsAction, ReapplyBreakpointsAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -24,12 +22,11 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { WorkbenchList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyEqualsExpr, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Gesture } from 'vs/base/browser/touch'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; @@ -38,6 +35,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, Action2, MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -57,11 +61,21 @@ export function getExpandedBodySize(model: IDebugModel, countLimit: number): num } type BreakpointItem = IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IExceptionBreakpoint; +interface InputBoxData { + breakpoint: IFunctionBreakpoint | IExceptionBreakpoint; + type: 'condition' | 'hitCount' | 'name'; +} + export class BreakpointsView extends ViewPane { private list!: WorkbenchList; private needsRefresh = false; private ignoreLayout = false; + private menu: IMenu; + private breakpointItemType: IContextKey; + private breakpointSupportsCondition: IContextKey; + private _inputBoxData: InputBoxData | undefined; + breakpointInputFocused: IContextKey; constructor( options: IViewletViewOptions, @@ -78,26 +92,32 @@ export class BreakpointsView extends ViewPane { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @ILabelService private readonly labelService: ILabelService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); + this._register(this.menu); + this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); + this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } - public renderBody(container: HTMLElement): void { + renderBody(container: HTMLElement): void { super.renderBody(container); this.element.classList.add('debug-pane'); container.classList.add('debug-breakpoints'); - const delegate = new BreakpointsDelegate(this.debugService); + const delegate = new BreakpointsDelegate(this); this.list = >this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ - this.instantiationService.createInstance(BreakpointsRenderer), - new ExceptionBreakpointsRenderer(this.debugService), - new ExceptionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService), - this.instantiationService.createInstance(FunctionBreakpointsRenderer), + this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointSupportsCondition), + new ExceptionBreakpointsRenderer(this.menu, this.breakpointSupportsCondition, this.debugService), + new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService), + this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition), this.instantiationService.createInstance(DataBreakpointsRenderer), - new FunctionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService, this.labelService) + new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService, this.labelService) ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, multipleSelectionSupport: false, @@ -124,7 +144,7 @@ export class BreakpointsView extends ViewPane { const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); this._register(resourceNavigator.onDidOpen(async e => { - if (e.element === null) { + if (!e.element) { return; } @@ -132,15 +152,12 @@ export class BreakpointsView extends ViewPane { return; } - const element = this.list.element(e.element); - - if (element instanceof Breakpoint) { - openBreakpointSource(element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); + if (e.element instanceof Breakpoint) { + openBreakpointSource(e.element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); } - if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedBreakpoint()) { + if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { // double click - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); + this.renderInputBox({ breakpoint: e.element, type: 'name' }); } })); @@ -158,13 +175,23 @@ export class BreakpointsView extends ViewPane { })); } - public focus(): void { + focus(): void { super.focus(); if (this.list) { this.list.domFocus(); } } + renderInputBox(data: InputBoxData | undefined): void { + this._inputBoxData = data; + this.onBreakpointsChange(); + this._inputBoxData = undefined; + } + + get inputBoxData(): InputBoxData | undefined { + return this._inputBoxData; + } + protected layoutBody(height: number, width: number): void { if (this.ignoreLayout) { return; @@ -183,68 +210,25 @@ export class BreakpointsView extends ViewPane { } private onListContextMenu(e: IListContextMenuEvent): void { - if (!e.element) { - return; - } - - const actions: IAction[] = []; const element = e.element; + const type = element instanceof Breakpoint ? 'breakpoint' : element instanceof ExceptionBreakpoint ? 'exceptionBreakpoint' : + element instanceof FunctionBreakpoint ? 'functionBreakpoint' : element instanceof DataBreakpoint ? 'dataBreakpoint' : undefined; + this.breakpointItemType.set(type); + const session = this.debugService.getViewModel().focusedSession; + const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); + this.breakpointSupportsCondition.set(conditionSupported); - const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('Logpoint', "Logpoint") : nls.localize('Breakpoint', "Breakpoint"); - if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) { - actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), '', true, async () => { - if (element instanceof Breakpoint) { - const editor = await openBreakpointSource(element, false, false, true, this.debugService, this.editorService); - if (editor) { - const codeEditor = editor.getControl(); - if (isCodeEditor(codeEditor)) { - codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(element.lineNumber, element.column); - } - } - } else { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - } - })); - actions.push(new Separator()); - } - if (element instanceof ExceptionBreakpoint && element.supportsCondition) { - actions.push(new Action('workbench.action.debug.editExceptionBreakpointCondition', nls.localize('editCondition', "Edit Condition..."), '', true, async () => { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - })); - actions.push(new Separator()); - } - - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); - - if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length > 1) { - actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Separator()); - - actions.push(new EnableAllBreakpointsAction(EnableAllBreakpointsAction.ID, EnableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new DisableAllBreakpointsAction(DisableAllBreakpointsAction.ID, DisableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - } - - actions.push(new Separator()); - actions.push(new ReapplyBreakpointsAction(ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL, this.debugService, this.keybindingService)); + const secondary: IAction[] = []; + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, g => /^inline/.test(g)); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => secondary, getActionsContext: () => element, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } - public getActions(): IAction[] { - return [ - new AddFunctionBreakpointAction(AddFunctionBreakpointAction.ID, AddFunctionBreakpointAction.LABEL, this.debugService, this.keybindingService), - new ToggleBreakpointsActivatedAction(ToggleBreakpointsActivatedAction.ID, ToggleBreakpointsActivatedAction.ACTIVATE_LABEL, this.debugService, this.keybindingService), - new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService) - ]; - } - private updateSize(): void { const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; @@ -281,7 +265,7 @@ export class BreakpointsView extends ViewPane { class BreakpointsDelegate implements IListVirtualDelegate { - constructor(private debugService: IDebugService) { + constructor(private view: BreakpointsView) { // noop } @@ -294,16 +278,16 @@ class BreakpointsDelegate implements IListVirtualDelegate { return BreakpointsRenderer.ID; } if (element instanceof FunctionBreakpoint) { - const selected = this.debugService.getViewModel().getSelectedBreakpoint(); - if (!element.name || (selected && selected.getId() === element.getId())) { + const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint; + if (!element.name || (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId())) { return FunctionBreakpointInputRenderer.ID; } return FunctionBreakpointsRenderer.ID; } if (element instanceof ExceptionBreakpoint) { - const selected = this.debugService.getViewModel().getSelectedBreakpoint(); - if (selected && selected.getId() === element.getId()) { + const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint; + if (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId()) { return ExceptionBreakpointInputRenderer.ID; } return ExceptionBreakpointsRenderer.ID; @@ -321,7 +305,9 @@ interface IBaseBreakpointTemplateData { name: HTMLElement; checkbox: HTMLInputElement; context: BreakpointItem; + actionBar: ActionBar; toDispose: IDisposable[]; + elementDisposable: IDisposable[]; } interface IBaseBreakpointWithIconTemplateData extends IBaseBreakpointTemplateData { @@ -337,26 +323,31 @@ interface IExceptionBreakpointTemplateData extends IBaseBreakpointTemplateData { condition: HTMLElement; } +interface IFunctionBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData { + condition: HTMLElement; +} + interface IFunctionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IFunctionBreakpoint; - reactedOnEvent: boolean; toDispose: IDisposable[]; + type: 'hitCount' | 'condition' | 'name'; } interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; breakpoint: IExceptionBreakpoint; - reactedOnEvent: boolean; toDispose: IDisposable[]; } class BreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -376,6 +367,7 @@ class BreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -386,6 +378,8 @@ class BreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); + } + + disposeElement(_element: IBreakpoint, _index: number, templateData: IBreakpointTemplateData): void { + dispose(templateData.elementDisposable); } disposeTemplate(templateData: IBreakpointTemplateData): void { @@ -422,6 +427,8 @@ class BreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, private debugService: IDebugService ) { // noop @@ -439,6 +446,7 @@ class ExceptionBreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -449,6 +457,8 @@ class ExceptionBreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); + } + + disposeElement(_element: IExceptionBreakpoint, _index: number, templateData: IExceptionBreakpointTemplateData): void { + dispose(templateData.elementDisposable); } disposeTemplate(templateData: IExceptionBreakpointTemplateData): void { @@ -466,9 +486,11 @@ class ExceptionBreakpointsRenderer implements IListRenderer { +class FunctionBreakpointsRenderer implements IListRenderer { constructor( + private menu: IMenu, + private breakpointSupportsCondition: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -481,13 +503,14 @@ class FunctionBreakpointsRenderer implements IListRenderer { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -496,11 +519,15 @@ class FunctionBreakpointsRenderer implements IListRenderer /^inline/.test(g))); + data.actionBar.clear(); + data.actionBar.push(primary, { icon: true, label: false }); } - disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { + disposeElement(_element: IFunctionBreakpoint, _index: number, templateData: IFunctionBreakpointTemplateData): void { + dispose(templateData.elementDisposable); + } + + disposeTemplate(templateData: IFunctionBreakpointTemplateData): void { dispose(templateData.toDispose); } } @@ -569,7 +611,7 @@ class DataBreakpointsRenderer implements IListRenderer { constructor( + private view: BreakpointsView, private debugService: IDebugService, private contextViewService: IContextViewService, private themeService: IThemeService, private labelService: ILabelService - ) { - // noop - } + ) { } static readonly ID = 'functionbreakpointinput'; @@ -604,22 +645,33 @@ class FunctionBreakpointInputRenderer implements IListRenderer { - if (!template.reactedOnEvent) { - template.reactedOnEvent = true; - this.debugService.getViewModel().setSelectedBreakpoint(undefined); - if (inputBox.value && (renamed || template.breakpoint.name)) { - this.debugService.renameFunctionBreakpoint(template.breakpoint.getId(), renamed ? inputBox.value : template.breakpoint.name); + const wrapUp = (success: boolean) => { + this.view.breakpointInputFocused.set(false); + const id = template.breakpoint.getId(); + + if (success) { + if (template.type === 'name') { + this.debugService.updateFunctionBreakpoint(id, { name: inputBox.value }); + } + if (template.type === 'condition') { + this.debugService.updateFunctionBreakpoint(id, { condition: inputBox.value }); + } + if (template.type === 'hitCount') { + this.debugService.updateFunctionBreakpoint(id, { hitCondition: inputBox.value }); + } + } else { + if (template.type === 'name' && !template.breakpoint.name) { + this.debugService.removeFunctionBreakpoints(id); } else { - this.debugService.removeFunctionBreakpoints(template.breakpoint.getId()); + this.view.renderInputBox(undefined); } } }; @@ -649,7 +701,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { data.inputBox.focus(); data.inputBox.select(); @@ -671,6 +738,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { constructor( + private view: BreakpointsView, private debugService: IDebugService, private contextViewService: IContextViewService, private themeService: IThemeService @@ -692,24 +760,22 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { - if (!template.reactedOnEvent) { - template.reactedOnEvent = true; - this.debugService.getViewModel().setSelectedBreakpoint(undefined); - let newCondition = template.breakpoint.condition; - if (success) { - newCondition = inputBox.value !== '' ? inputBox.value : undefined; - } - this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition); + this.view.breakpointInputFocused.set(false); + let newCondition = template.breakpoint.condition; + if (success) { + newCondition = inputBox.value !== '' ? inputBox.value : undefined; } + this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition); }; toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { @@ -735,7 +801,6 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + const debugService = accessor.get(IDebugService); + if (breakpoint instanceof Breakpoint) { + await debugService.removeBreakpoints(breakpoint.getId()); + } else if (breakpoint instanceof FunctionBreakpoint) { + await debugService.removeFunctionBreakpoints(breakpoint.getId()); + } else if (breakpoint instanceof DataBreakpoint) { + await debugService.removeDataBreakpoints(breakpoint.getId()); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.removeAllBreakpoints', + title: { + original: 'Remove All Breakpoints', + value: localize('removeAllBreakpoints', "Remove All Breakpoints"), + mnemonicTitle: localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + f1: true, + icon: icons.breakpointsRemoveAll, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 30, + when: ContextKeyEqualsExpr.create('view', BREAKPOINTS_VIEW_ID) + }, { + id: MenuId.DebugBreakpointsContext, + group: '3_modification', + order: 20, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.removeBreakpoints(); + debugService.removeFunctionBreakpoints(); + debugService.removeDataBreakpoints(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.enableAllBreakpoints', + title: { + original: '', + value: localize('enableAllBreakpoints', "Enable All Breakpoints"), + mnemonicTitle: localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints"), + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 10, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.enableOrDisableBreakpoints(true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.disableAllBreakpoints', + title: { + original: 'Disable All Breakpoints', + value: localize('disableAllBreakpoints', "Disable All Breakpoints"), + mnemonicTitle: localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 20, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }, { + id: MenuId.MenubarDebugMenu, + group: '5_breakpoints', + order: 2, + + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.enableOrDisableBreakpoints(false); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.reapplyBreakpointsAction', + title: localize('reapplyAllBreakpoints', "Reapply All Breakpoints"), + f1: true, + precondition: CONTEXT_IN_DEBUG_MODE, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'z_commands', + order: 30, + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINT_ITEM_TYPE.notEqualsTo('exceptionBreakpoint')) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + await debugService.setBreakpointsActivated(true); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editBreakpoint', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editCondition', "Edit Condition..."), + icon: Codicon.edit, + precondition: CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'navigation', + order: 10 + }, { + id: MenuId.DebugBreakpointsContext, + group: 'inline', + order: 10 + }] + }); + } + + async runInView(accessor: ServicesAccessor, view: BreakpointsView, breakpoint: ExceptionBreakpoint | Breakpoint | FunctionBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + if (breakpoint instanceof Breakpoint) { + const editor = await openBreakpointSource(breakpoint, false, false, true, debugService, editorService); + if (editor) { + const codeEditor = editor.getControl(); + if (isCodeEditor(codeEditor)) { + codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(breakpoint.lineNumber, breakpoint.column); + } + } + } else { + view.renderInputBox({ breakpoint, type: 'condition' }); + } + } +}); + + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editFunctionBreakpoint', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editBreakpoint', "Edit Function Breakpoint..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: '1_breakpoints', + order: 10, + when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint') + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: BreakpointsView, breakpoint: IFunctionBreakpoint) { + view.renderInputBox({ breakpoint, type: 'name' }); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.editFunctionBreakpointHitCount', + viewId: BREAKPOINTS_VIEW_ID, + title: localize('editHitCount', "Edit Hit Count..."), + precondition: CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, + menu: [{ + id: MenuId.DebugBreakpointsContext, + group: 'navigation', + order: 20, + when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint') + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: BreakpointsView, breakpoint: IFunctionBreakpoint) { + view.renderInputBox({ breakpoint, type: 'hitCount' }); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index fbf117cf0b..863f657019 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -127,17 +127,21 @@ export class CallStackEditorContribution implements IEditorContribution { const isSessionFocused = s === focusedStackFrame?.thread.session; s.getAllThreads().forEach(t => { if (t.stopped) { - let candidateStackFrame = t === focusedStackFrame?.thread ? focusedStackFrame : undefined; - if (!candidateStackFrame) { - const callStack = t.getCallStack(); - if (callStack.length) { - candidateStackFrame = callStack[0]; + const callStack = t.getCallStack(); + const stackFrames: IStackFrame[] = []; + if (callStack.length > 0) { + // Always decorate top stack frame, and decorate focused stack frame if it is not the top stack frame + if (focusedStackFrame && !focusedStackFrame.equals(callStack[0])) { + stackFrames.push(focusedStackFrame); } + stackFrames.push(callStack[0]); } - if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { - decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); - } + stackFrames.forEach(candidateStackFrame => { + if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); + } + }); } }); }); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 9070565d4e..8e294c315e 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -3,22 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel, CALLSTACK_VIEW_ID, CONTEXT_DEBUG_STATE, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction, registerAction2 } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IContextKey, IContextKeyService, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -33,7 +32,6 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -47,6 +45,8 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -162,7 +162,7 @@ export class CallStackView extends ViewPane { this.stateMessageLabel.classList.toggle('exception', thread.stoppedDetails.reason === 'exception'); this.stateMessage.hidden = false; } else if (sessions.length === 1 && sessions[0].state === State.Running) { - this.stateMessageLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); + this.stateMessageLabel.textContent = localize({ key: 'running', comment: ['indicates state'] }, "Running"); this.stateMessageLabel.title = sessions[0].getLabel(); this.stateMessageLabel.classList.remove('exception'); this.stateMessage.hidden = false; @@ -205,14 +205,6 @@ export class CallStackView extends ViewPane { this.stateMessageLabel = dom.append(this.stateMessage, $('span.label')); } - getActions(): IAction[] { - if (this.stateMessage.hidden) { - return [new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(icons.debugCollapseAll))]; - } - - return []; - } - renderBody(container: HTMLElement): void { super.renderBody(container); this.element.classList.add('debug-pane'); @@ -220,11 +212,11 @@ export class CallStackView extends ViewPane { const treeContainer = renderViewTree(container); this.dataSource = new CallStackDataSource(this.debugService); - const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu); + const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu, this.callStackItemType); this.tree = >this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [ sessionsRenderer, - new ThreadsRenderer(this.instantiationService), - this.instantiationService.createInstance(StackFramesRenderer), + new ThreadsRenderer(this.callStackItemType, this.instantiationService), + this.instantiationService.createInstance(StackFramesRenderer, this.callStackItemType), new ErrorsRenderer(), new LoadAllRenderer(this.themeService), new ShowMoreRenderer(this.themeService) @@ -259,7 +251,7 @@ export class CallStackView extends ViewPane { return LoadAllRenderer.LABEL; } - return nls.localize('showMoreStackFrames2', "Show More Stack Frames"); + return localize('showMoreStackFrames2', "Show More Stack Frames"); }, getCompressedNodeKeyboardNavigationLabel: (e: CallStackItem[]) => { const firstItem = e[0]; @@ -378,6 +370,10 @@ export class CallStackView extends ViewPane { this.tree.domFocus(); } + collapseAll(): void { + this.tree.collapseAll(); + } + private async updateTreeSelection(): Promise { if (!this.tree || !this.tree.getInput()) { // Tree not initialized yet @@ -493,6 +489,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer, @IInstantiationService private readonly instantiationService: IInstantiationService ) { } @@ -532,7 +529,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer t.stopped); @@ -542,6 +539,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer /^inline/.test(g))); data.actionBar.clear(); @@ -557,7 +555,7 @@ class SessionsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'thread'; - constructor(private readonly instantiationService: IInstantiationService) { } + constructor( + private callStackItemType: IContextKey, + private readonly instantiationService: IInstantiationService + ) { } get templateId(): string { return ThreadsRenderer.ID; @@ -591,11 +592,12 @@ class ThreadsRenderer implements ICompressibleTreeRenderer, index: number, data: IThreadTemplateData): void { const thread = element.element; - data.thread.title = nls.localize('thread', "Thread"); + data.thread.title = localize('thread', "Thread"); data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.actionBar.clear(); + this.callStackItemType.set('thread'); const actions = getActions(this.instantiationService, thread); data.actionBar.push(actions, { icon: true, label: false }); } @@ -613,8 +615,9 @@ class StackFramesRenderer implements ICompressibleTreeRenderer, @ILabelService private readonly labelService: ILabelService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, ) { } get templateId(): string { @@ -659,8 +662,9 @@ class StackFramesRenderer implements ICompressibleTreeRenderer { + const action = new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => { try { await stackFrame.restart(); } catch (e) { @@ -710,7 +714,7 @@ class ErrorsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'loadAll'; - static readonly LABEL = nls.localize('loadAllStackFrames', "Load All Stack Frames"); + static readonly LABEL = localize('loadAllStackFrames', "Load All Stack Frames"); constructor(private readonly themeService: IThemeService) { } @@ -766,9 +770,9 @@ class ShowMoreRenderer implements ICompressibleTreeRenderer, index: number, data: ILabelTemplateData): void { const stackFrames = element.element; if (stackFrames.every(sf => !!(sf.source && sf.source.origin && sf.source.origin === stackFrames[0].source.origin))) { - data.label.textContent = nls.localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin); + data.label.textContent = localize('showMoreAndOrigin', "Show {0} More: {1}", stackFrames.length, stackFrames[0].source.origin); } else { - data.label.textContent = nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length); + data.label.textContent = localize('showMoreStackFrames', "Show {0} More Stack Frames", stackFrames.length); } } @@ -930,26 +934,26 @@ class CallStackDataSource implements IAsyncDataSource { getWidgetAriaLabel(): string { - return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"); + return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'callStackAriaLabel' }, "Debug Call Stack"); } getAriaLabel(element: CallStackItem): string { if (element instanceof Thread) { - return nls.localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel); + return localize({ key: 'threadAriaLabel', comment: ['Placeholders stand for the thread name and the thread state.For example "Thread 1" and "Stopped'] }, "Thread {0} {1}", element.name, element.stateLabel); } if (element instanceof StackFrame) { - return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element)); + return localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}", element.name, element.range.startLineNumber, getSpecificSourceName(element)); } if (isDebugSession(element)) { const thread = element.getAllThreads().find(t => t.stopped); - const state = thread ? thread.stateLabel : nls.localize({ key: 'running', comment: ['indicates state'] }, "Running"); - return nls.localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); + const state = thread ? thread.stateLabel : localize({ key: 'running', comment: ['indicates state'] }, "Running"); + return localize({ key: 'sessionLabel', comment: ['Placeholders stand for the session name and the session state. For example "Launch Program" and "Running"'] }, "Session {0} {1}", element.getLabel(), state); } if (typeof element === 'string') { return element; } if (element instanceof Array) { - return nls.localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length); + return localize('showMoreStackFrames', "Show {0} More Stack Frames", element.length); } // element instanceof ThreadAndSessionIds @@ -1121,3 +1125,26 @@ class CallStackCompressionDelegate implements ITreeCompressionDelegate { + constructor() { + super({ + id: 'callStack.collapse', + viewId: CALLSTACK_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + precondition: CONTEXT_DEBUG_STATE.isEqualTo(getStateLabel(State.Stopped)), + menu: { + id: MenuId.ViewTitle, + order: 10, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', CALLSTACK_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: CallStackView) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index c6c0d353bf..8c2ec44384 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -7,22 +7,20 @@ import 'vs/css!./media/debug.contribution'; import 'vs/css!./media/debugHover'; import * as nls from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionRegistryExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { BreakpointsView } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, } from 'vs/workbench/contrib/debug/common/debug'; -import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; -import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -32,14 +30,13 @@ import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debu import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; -import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; -import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; +import { RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; +import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; +import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { DebugViewPaneContainer, OpenDebugConsoleAction, OpenDebugViewletAction } from 'vs/workbench/contrib/debug/browser/debugViewlet'; +import { DebugViewPaneContainer } from 'vs/workbench/contrib/debug/browser/debugViewlet'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { CallStackEditorContribution } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { BreakpointEditorContribution } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; @@ -54,9 +51,7 @@ import { DebugEditorContribution } from 'vs/workbench/contrib/debug/browser/debu import { FileAccess } from 'vs/base/common/network'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); const debugCategory = nls.localize('debugCategory', "Debug"); -const runCategroy = nls.localize('runCategory', "Run"); registerWorkbenchContributions(); registerColors(); registerCommandsAndActions(); @@ -64,8 +59,6 @@ registerDebugMenu(); registerEditorActions(); registerCommands(); registerDebugPanel(); -registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE); registerSingleton(IDebugService, DebugService, true); registerDebugView(); @@ -102,18 +95,10 @@ function regsiterEditorContributions(): void { function registerCommandsAndActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); - const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), + group: debugCategory, command: { id, title: `Debug: ${title}`, @@ -136,33 +121,8 @@ function registerCommandsAndActions(): void { registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); - - // Debug toolbar - - const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { - MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { - group: 'navigation', - when, - order, - command: { - id, - title, - icon, - precondition - } - }); - }; - - registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); - registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); - registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH); - registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); - registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugCommandPaletteItem(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); + registerDebugCommandPaletteItem(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); // Debug callstack context menu const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { @@ -194,6 +154,12 @@ function registerCommandsAndActions(): void { registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 100, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, 'z_commands'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, BREAK_WHEN_VALUE_CHANGES_ID, nls.localize('breakWhenValueChanges', "Break When Value Changes"), 200, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, undefined, 'z_commands'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, ADD_WATCH_ID, ADD_WATCH_LABEL, 10, undefined, undefined, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, nls.localize('editWatchExpression', "Edit Expression"), 20, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 30, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands'); + registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands'); + // Touch Bar if (isMacintosh) { @@ -210,8 +176,8 @@ function registerCommandsAndActions(): void { }); }; - registerTouchBarEntry(StartAction.ID, StartAction.LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); - registerTouchBarEntry(RunAction.ID, RunAction.LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png', require)); + registerTouchBarEntry(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, 0, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); + registerTouchBarEntry(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, 1, CONTEXT_IN_DEBUG_MODE.toNegated(), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-without-debugging-tb.png', require)); registerTouchBarEntry(CONTINUE_ID, CONTINUE_LABEL, 0, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/continue-tb.png', require)); registerTouchBarEntry(PAUSE_ID, PAUSE_LABEL, 1, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.notEquals('debugState', 'stopped')), FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/pause-tb.png', require)); registerTouchBarEntry(STEP_OVER_ID, STEP_OVER_LABEL, 2, CONTEXT_IN_DEBUG_MODE, FileAccess.asFileUri('vs/workbench/contrib/debug/browser/media/stepover-tb.png', require)); @@ -232,15 +198,6 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miViewRun', comment: ['&& denotes a mnemonic'] }, "&&Run") }, order: 4 - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: OpenDebugConsoleAction.ID, - title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") - }, - order: 2 }); {{SQL CARBON EDIT}} - Disable unusued menus */ // Debug menu @@ -248,7 +205,7 @@ function registerDebugMenu(): void { MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '1_debug', command: { - id: StartAction.ID, + id: DEBUG_START_COMMAND_ID, title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") }, order: 1, @@ -258,7 +215,7 @@ function registerDebugMenu(): void { MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '1_debug', command: { - id: RunAction.ID, + id: DEBUG_RUN_COMMAND_ID, title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging") }, order: 2, @@ -288,15 +245,6 @@ function registerDebugMenu(): void { }); // Configuration - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '2_configuration', - command: { - id: ConfigureAction.ID, - title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '2_configuration', @@ -354,25 +302,6 @@ function registerDebugMenu(): void { }); // New Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '4_new_breakpoint', - command: { - id: TOGGLE_BREAKPOINT_ID, - title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { group: '1_breakpoints', @@ -384,26 +313,6 @@ function registerDebugMenu(): void { when: CONTEXT_DEBUGGERS_AVAILABLE }); - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: AddFunctionBreakpointAction.ID, - title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") - }, - order: 3, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { - group: '1_breakpoints', - command: { - id: ADD_LOG_POINT_ID, - title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") - }, - order: 4, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '4_new_breakpoint', title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), @@ -412,36 +321,7 @@ function registerDebugMenu(): void { when: CONTEXT_DEBUGGERS_AVAILABLE }); - // Modify Breakpoints - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: EnableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints") - }, - order: 1, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: DisableAllBreakpointsAction.ID, - title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") - }, - order: 2, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { - group: '5_breakpoints', - command: { - id: RemoveAllBreakpointsAction.ID, - title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") - }, - order: 3, - when: CONTEXT_DEBUGGERS_AVAILABLE - }); + // Breakpoint actions are registered from breakpointsView.ts // Install Debuggers MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -459,14 +339,12 @@ function registerDebugPanel(): void { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: DEBUG_PANEL_ID, - name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), + title: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), icon: icons.debugConsoleViewIcon, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: DEBUG_PANEL_ID, - focusCommand: { id: OpenDebugConsoleAction.ID }, - order: 2, - hideIfEmpty: true - }, ViewContainerLocation.Panel); + hideIfEmpty: true, + }, ViewContainerLocation.Panel, { donotRegisterOpenCommand: true }); Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ id: REPL_VIEW_ID, @@ -476,22 +354,31 @@ function registerDebugPanel(): void { canMoveView: true, when: CONTEXT_DEBUGGERS_AVAILABLE, ctorDescriptor: new SyncDescriptor(Repl), + openCommandActionDescriptor: { + id: 'workbench.debug.action.toggleRepl', + mnemonicTitle: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }, + order: 2 + } }], VIEW_CONTAINER); - - registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', CATEGORIES.View.value, CONTEXT_DEBUGGERS_AVAILABLE); } function registerDebugView(): void { const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, - name: nls.localize('run', "Run"), + title: nls.localize('run and debug', "Run and Debug"), + openCommandActionDescriptor: { + id: VIEWLET_ID, + mnemonicTitle: nls.localize({ key: 'miViewRun', comment: ['&& denotes a mnemonic'] }, "&&Run"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D }, + order: 13 + }, ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), icon: icons.runViewIcon, alwaysUseContainerInfo: true, order: 13 // {{SQL CARBON EDIT}} }, ViewContainerLocation.Sidebar); - registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugViewletAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D }), 'View: Show Run and Debug', CATEGORIES.View.value); // Register default debug views const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); @@ -579,6 +466,11 @@ function registerConfiguration(): void { description: nls.localize('debug.console.historySuggestions', "Controls if the debug console should suggest previously typed input."), default: true }, + 'debug.console.collapseIdenticalLines': { + type: 'boolean', + description: nls.localize('debug.console.collapseIdenticalLines', "Controls if the debug console should collapse identical lines and show a number of occurrences with a badge."), + default: true + }, 'launch': { type: 'object', description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces."), @@ -609,3 +501,4 @@ function registerConfiguration(): void { } }); } + diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 3c65c5e7aa..1eb6b3e022 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -11,7 +11,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch, State } from 'vs/workbench/contrib/debug/common/debug'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { selectBorder, selectBackground } from 'vs/platform/theme/common/colorRegistry'; @@ -76,7 +76,9 @@ export class StartDebugActionViewItem implements IActionViewItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => { this.start.blur(); - this.actionRunner.run(this.action, this.context); + if (this.debugService.state !== State.Initializing) { + this.actionRunner.run(this.action, this.context); + } })); this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => { @@ -93,7 +95,7 @@ export class StartDebugActionViewItem implements IActionViewItem { this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter)) { + if (event.equals(KeyCode.Enter) && this.debugService.state !== State.Initializing) { this.actionRunner.run(this.action, this.context); } if (event.equals(KeyCode.RightArrow)) { diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts deleted file mode 100644 index 402209b86e..0000000000 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ /dev/null @@ -1,421 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Breakpoint, FunctionBreakpoint, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { deepClone } from 'vs/base/common/objects'; -import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; - -export abstract class AbstractDebugAction extends Action { - - constructor( - id: string, label: string, cssClass: string, - @IDebugService protected debugService: IDebugService, - @IKeybindingService protected keybindingService: IKeybindingService, - ) { - super(id, label, cssClass, false); - this._register(this.debugService.onDidChangeState(state => this.updateEnablement(state))); - - this.updateLabel(label); - this.updateEnablement(); - } - - run(_: any): Promise { - throw new Error('implement me'); - } - - get tooltip(): string { - const keybinding = this.keybindingService.lookupKeybinding(this.id); - const keybindingLabel = keybinding && keybinding.getLabel(); - - return keybindingLabel ? `${this.label} (${keybindingLabel})` : this.label; - } - - protected updateLabel(newLabel: string): void { - this.label = newLabel; - } - - protected updateEnablement(state = this.debugService.state): void { - this.enabled = this.isEnabled(state); - } - - protected isEnabled(_: State): boolean { - return true; - } -} - -export class ConfigureAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.configure'; - static readonly LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService private readonly notificationService: INotificationService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.debugConfigure), debugService, keybindingService); - this._register(debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateClass())); - this.updateClass(); - } - - get tooltip(): string { - if (this.debugService.getConfigurationManager().selectedConfiguration.name) { - return ConfigureAction.LABEL; - } - - return nls.localize('launchJsonNeedsConfigurtion', "Configure or Fix 'launch.json'"); - } - - private updateClass(): void { - const configurationManager = this.debugService.getConfigurationManager(); - this.class = configurationManager.selectedConfiguration.name ? 'debug-action' + ThemeIcon.asClassName(icons.debugConfigure) : 'debug-action ' + ThemeIcon.asClassName(icons.debugConfigure) + ' notification'; - } - - async run(): Promise { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY || this.contextService.getWorkspace().folders.length === 0) { - this.notificationService.info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration.")); - return; - } - - const configurationManager = this.debugService.getConfigurationManager(); - let launch: ILaunch | undefined; - if (configurationManager.selectedConfiguration.name) { - launch = configurationManager.selectedConfiguration.launch; - } else { - const launches = configurationManager.getLaunches().filter(l => !l.hidden); - if (launches.length === 1) { - launch = launches[0]; - } else { - const picks = launches.map(l => ({ label: l.name, launch: l })); - const picked = await this.quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { - activeItem: picks[0], - placeHolder: nls.localize({ key: 'selectWorkspaceFolder', comment: ['User picks a workspace folder or a workspace configuration file here. Workspace configuration files can contain settings and thus a launch.json configuration can be written into one.'] }, "Select a workspace folder to create a launch.json file in or add it to the workspace config file") - }); - if (picked) { - launch = picked.launch; - } - } - } - - if (launch) { - return launch.openConfigFile(false); - } - } -} - -export class StartAction extends AbstractDebugAction { - static ID = 'workbench.action.debug.start'; - static LABEL = nls.localize('startDebug', "Start Debugging"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - ) { - super(id, label, 'debug-action start', debugService, keybindingService); - - this._register(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateEnablement())); - this._register(this.debugService.onDidNewSession(() => this.updateEnablement())); - this._register(this.debugService.onDidEndSession(() => this.updateEnablement())); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateEnablement())); - } - - async run(): Promise { - let { launch, name, getConfig } = this.debugService.getConfigurationManager().selectedConfiguration; - const config = await getConfig(); - const clonedConfig = deepClone(config); - return this.debugService.startDebugging(launch, clonedConfig || name, { noDebug: this.isNoDebug() }); - } - - protected isNoDebug(): boolean { - return false; - } - - static isEnabled(debugService: IDebugService) { - const sessions = debugService.getModel().getSessions(); - - if (debugService.state === State.Initializing) { - return false; - } - let { name, launch } = debugService.getConfigurationManager().selectedConfiguration; - let nameToStart = name; - - if (sessions.some(s => s.configuration.name === nameToStart && s.root === launch?.workspace)) { - // There is already a debug session running and we do not have any launch configuration selected - return false; - } - - return true; - } - - // Disabled if the launch drop down shows the launch config that is already running. - protected isEnabled(): boolean { - return StartAction.isEnabled(this.debugService); - } -} - -export class RunAction extends StartAction { - static readonly ID = 'workbench.action.debug.run'; - static LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); - - protected isNoDebug(): boolean { - return true; - } -} - -export class SelectAndStartAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.selectandstart'; - static readonly LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label, '', debugService, keybindingService); - } - - async run(): Promise { - this.quickInputService.quickAccess.show('debug '); - } -} - -export class RemoveBreakpointAction extends Action { - static readonly ID = 'workbench.debug.viewlet.action.removeBreakpoint'; - static readonly LABEL = nls.localize('removeBreakpoint', "Remove Breakpoint"); - - constructor(id: string, label: string, @IDebugService private readonly debugService: IDebugService) { - super(id, label, 'debug-action remove'); - } - - run(breakpoint: IBreakpoint): Promise { - return breakpoint instanceof Breakpoint ? this.debugService.removeBreakpoints(breakpoint.getId()) - : breakpoint instanceof FunctionBreakpoint ? this.debugService.removeFunctionBreakpoints(breakpoint.getId()) : this.debugService.removeDataBreakpoints(breakpoint.getId()); - } -} - -export class RemoveAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.removeAllBreakpoints'; - static readonly LABEL = nls.localize('removeAllBreakpoints', "Remove All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.breakpointsRemoveAll), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return Promise.all([this.debugService.removeBreakpoints(), this.debugService.removeFunctionBreakpoints(), this.debugService.removeDataBreakpoints()]); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (model.getBreakpoints().length > 0 || model.getFunctionBreakpoints().length > 0 || model.getDataBreakpoints().length > 0); - } -} - -export class EnableAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.enableAllBreakpoints'; - static readonly LABEL = nls.localize('enableAllBreakpoints', "Enable All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action enable-all-breakpoints', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.enableOrDisableBreakpoints(true); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => !bp.enabled); - } -} - -export class DisableAllBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.disableAllBreakpoints'; - static readonly LABEL = nls.localize('disableAllBreakpoints', "Disable All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action disable-all-breakpoints', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.enableOrDisableBreakpoints(false); - } - - protected isEnabled(_: State): boolean { - const model = this.debugService.getModel(); - return (>model.getBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getExceptionBreakpoints()).some(bp => bp.enabled); - } -} - -export class ToggleBreakpointsActivatedAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.toggleBreakpointsActivatedAction'; - static readonly ACTIVATE_LABEL = nls.localize('activateBreakpoints', "Activate Breakpoints"); - static readonly DEACTIVATE_LABEL = nls.localize('deactivateBreakpoints', "Deactivate Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.breakpointsActivate), debugService, keybindingService); - this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => { - this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - this.updateEnablement(); - })); - } - - run(): Promise { - return this.debugService.setBreakpointsActivated(!this.debugService.getModel().areBreakpointsActivated()); - } - - protected isEnabled(_: State): boolean { - return !!(this.debugService.getModel().getFunctionBreakpoints().length || this.debugService.getModel().getBreakpoints().length || this.debugService.getModel().getDataBreakpoints().length); - } -} - -export class ReapplyBreakpointsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.reapplyBreakpointsAction'; - static readonly LABEL = nls.localize('reapplyAllBreakpoints', "Reapply All Breakpoints"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, '', debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - run(): Promise { - return this.debugService.setBreakpointsActivated(true); - } - - protected isEnabled(state: State): boolean { - const model = this.debugService.getModel(); - return (state === State.Running || state === State.Stopped) && - ((model.getFunctionBreakpoints().length + model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getDataBreakpoints().length) > 0); - } -} - -export class AddFunctionBreakpointAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.addFunctionBreakpointAction'; - static readonly LABEL = nls.localize('addFunctionBreakpoint', "Add Function Breakpoint"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsAddFuncBreakpoint), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.addFunctionBreakpoint(); - } - - protected isEnabled(_: State): boolean { - return !this.debugService.getViewModel().getSelectedBreakpoint() - && this.debugService.getModel().getFunctionBreakpoints().every(fbp => !!fbp.name); - } -} - -export class AddWatchExpressionAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.addWatchExpression'; - static readonly LABEL = nls.localize('addWatchExpression', "Add Expression"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsAdd), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); - this._register(this.debugService.getViewModel().onDidSelectExpression(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.addWatchExpression(); - } - - protected isEnabled(_: State): boolean { - const focusedExpression = this.debugService.getViewModel().getSelectedExpression(); - return this.debugService.getModel().getWatchExpressions().every(we => !!we.name && we !== focusedExpression); - } -} - -export class RemoveAllWatchExpressionsAction extends AbstractDebugAction { - static readonly ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions'; - static readonly LABEL = nls.localize('removeAllWatchExpressions', "Remove All Expressions"); - - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(icons.watchExpressionsRemoveAll), debugService, keybindingService); - this._register(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); - } - - async run(): Promise { - this.debugService.removeWatchExpressions(); - } - - protected isEnabled(_: State): boolean { - return this.debugService.getModel().getWatchExpressions().length > 0; - } -} - -export class FocusSessionAction extends AbstractDebugAction { - static readonly ID = 'workbench.action.debug.focusProcess'; - static readonly LABEL = nls.localize('focusSession', "Focus Session"); - - constructor(id: string, label: string, - @IDebugService debugService: IDebugService, - @IKeybindingService keybindingService: IKeybindingService, - @IEditorService private readonly editorService: IEditorService - ) { - super(id, label, '', debugService, keybindingService); - } - - async run(session: IDebugSession): Promise { - await this.debugService.focusStackFrame(undefined, undefined, session, true); - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - if (stackFrame) { - await stackFrame.openInEditor(this.editorService, true); - } - } -} - -export class CopyValueAction extends Action { - static readonly ID = 'workbench.debug.viewlet.action.copyValue'; - static readonly LABEL = nls.localize('copyValue', "Copy Value"); - - constructor( - id: string, label: string, private value: Variable | Expression, private context: string, - @IDebugService private readonly debugService: IDebugService, - @IClipboardService private readonly clipboardService: IClipboardService - ) { - super(id, label); - this._enabled = (this.value instanceof Expression) || (this.value instanceof Variable && !!this.value.evaluateName); - } - - async run(): Promise { - const stackFrame = this.debugService.getViewModel().focusedStackFrame; - const session = this.debugService.getViewModel().focusedSession; - if (!stackFrame || !session) { - return; - } - - const context = session.capabilities.supportsClipboardContext ? 'clipboard' : this.context; - const toEvaluate = this.value instanceof Variable ? (this.value.evaluateName || this.value.value) : this.value.name; - - try { - const evaluation = await session.evaluate(toEvaluate, stackFrame.frameId, context); - if (evaluation) { - this.clipboardService.writeText(evaluation.body.result); - } - } catch (e) { - this.clipboardService.writeText(typeof this.value === 'string' ? this.value : this.value.value); - } - } -} diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 740e1b38bc..725d84ac96 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -138,10 +138,10 @@ export class AdapterManager implements IAdapterManager { return Promise.resolve(config); } - runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments): Promise { + runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise { let factory = this.debugAdapterFactories.get(debugType); if (factory) { - return factory.runInTerminal(args); + return factory.runInTerminal(args, sessionId); } return Promise.resolve(void 0); } diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index 8ea354b6f1..80056ed784 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -4,8 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { localize } from 'vs/nls'; +import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; + +export const debugToolBarBackground = registerColor('debugToolBar.background', { + dark: '#333333', + light: '#F3F3F3', + hc: '#000000' +}, localize('debugToolBarBackground', "Debug toolbar background color.")); + +export const debugToolBarBorder = registerColor('debugToolBar.border', { + dark: null, + light: null, + hc: null +}, localize('debugToolBarBorder', "Debug toolbar border color.")); + +export const debugIconStartForeground = registerColor('debugIcon.startForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' +}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); export function registerColors() { @@ -28,6 +48,62 @@ export function registerColors() { const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + + + const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); + + const debugIconStopForeground = registerColor('debugIcon.stopForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' + }, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); + + const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' + }, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); + + const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' + }, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); + + const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); + + const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); + + const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); + + const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); + + const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' + }, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); + registerThemingParticipant((theme, collector) => { // All these colours provide a default value so they will never be undefined, hence the `!` const badgeBackgroundColor = theme.getColor(badgeBackground)!; @@ -186,5 +262,55 @@ export function registerColors() { } `); } + + const debugIconStartColor = theme.getColor(debugIconStartForeground); + if (debugIconStartColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStart)} { color: ${debugIconStartColor} !important; }`); + } + + const debugIconPauseColor = theme.getColor(debugIconPauseForeground); + if (debugIconPauseColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugPause)} { color: ${debugIconPauseColor} !important; }`); + } + + const debugIconStopColor = theme.getColor(debugIconStopForeground); + if (debugIconStopColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStop)} { color: ${debugIconStopColor} !important; }`); + } + + const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); + if (debugIconDisconnectColor) { + collector.addRule(`.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor} !important; }`); + } + + const debugIconRestartColor = theme.getColor(debugIconRestartForeground); + if (debugIconRestartColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestart)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestartFrame)} { color: ${debugIconRestartColor} !important; }`); + } + + const debugIconStepOverColor = theme.getColor(debugIconStepOverForeground); + if (debugIconStepOverColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOver)} { color: ${debugIconStepOverColor} !important; }`); + } + + const debugIconStepIntoColor = theme.getColor(debugIconStepIntoForeground); + if (debugIconStepIntoColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepInto)} { color: ${debugIconStepIntoColor} !important; }`); + } + + const debugIconStepOutColor = theme.getColor(debugIconStepOutForeground); + if (debugIconStepOutColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOut)} { color: ${debugIconStepOutColor} !important; }`); + } + + const debugIconContinueColor = theme.getColor(debugIconContinueForeground); + if (debugIconContinueColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugContinue)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugReverseContinue)} { color: ${debugIconContinueColor} !important; }`); + } + + const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); + if (debugIconStepBackColor) { + collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepBack)} { color: ${debugIconStepBackColor} !important; }`); + } }); } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 45a625ef98..dcfec9470f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -9,7 +9,7 @@ import { List } from 'vs/base/browser/ui/list/listWidget'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -23,12 +23,13 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { PanelFocusContext } from 'vs/workbench/common/panel'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IViewsService } from 'vs/workbench/common/views'; +import { deepClone } from 'vs/base/common/objects'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -47,6 +48,13 @@ export const RESTART_FRAME_ID = 'workbench.action.debug.restartFrame'; export const CONTINUE_ID = 'workbench.action.debug.continue'; export const FOCUS_REPL_ID = 'workbench.debug.action.focusRepl'; export const JUMP_TO_CURSOR_ID = 'debug.jumpToCursor'; +export const FOCUS_SESSION_ID = 'workbench.action.debug.focusProcess'; +export const SELECT_AND_START_ID = 'workbench.action.debug.selectandstart'; +export const DEBUG_CONFIGURE_COMMAND_ID = 'workbench.action.debug.configure'; +export const DEBUG_START_COMMAND_ID = 'workbench.action.debug.start'; +export const DEBUG_RUN_COMMAND_ID = 'workbench.action.debug.run'; +export const EDIT_EXPRESSION_COMMAND_ID = 'debug.renameWatchExpression'; +export const REMOVE_EXPRESSION_COMMAND_ID = 'debug.removeWatchExpression'; export const RESTART_LABEL = nls.localize('restartDebug', "Restart"); export const STEP_OVER_LABEL = nls.localize('stepOverDebug', "Step Over"); @@ -56,6 +64,11 @@ export const PAUSE_LABEL = nls.localize('pauseDebug', "Pause"); export const DISCONNECT_LABEL = nls.localize('disconnect', "Disconnect"); export const STOP_LABEL = nls.localize('stop', "Stop"); export const CONTINUE_LABEL = nls.localize('continueDebug', "Continue"); +export const FOCUS_SESSION_LABEL = nls.localize('focusSession', "Focus Session"); +export const SELECT_AND_START_LABEL = nls.localize('selectAndStartDebugging', "Select and Start Debugging"); +export const DEBUG_CONFIGURE_LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); +export const DEBUG_START_LABEL = nls.localize('startDebug', "Start Debugging"); +export const DEBUG_RUN_LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); interface CallStackContext { sessionId: string; @@ -322,7 +335,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CONTINUE_ID, - weight: KeybindingWeight.WorkbenchContrib, + weight: KeybindingWeight.WorkbenchContrib + 10, // Use a stronger weight to get priority over start debugging F5 shortcut primary: KeyCode.F5, when: CONTEXT_IN_DEBUG_MODE, handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { @@ -346,6 +359,53 @@ export function registerCommands(): void { } }); + CommandsRegistry.registerCommand({ + id: FOCUS_SESSION_ID, + handler: async (accessor: ServicesAccessor, session: IDebugSession) => { + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + await debugService.focusStackFrame(undefined, undefined, session, true); + const stackFrame = debugService.getViewModel().focusedStackFrame; + if (stackFrame) { + await stackFrame.openInEditor(editorService, true); + } + } + }); + + CommandsRegistry.registerCommand({ + id: SELECT_AND_START_ID, + handler: async (accessor: ServicesAccessor) => { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.quickAccess.show('debug '); + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DEBUG_START_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F5, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + handler: async (accessor: ServicesAccessor, debugStartOptions?: { noDebug: boolean }) => { + const debugService = accessor.get(IDebugService); + let { launch, name, getConfig } = debugService.getConfigurationManager().selectedConfiguration; + const config = await getConfig(); + const clonedConfig = deepClone(config); + await debugService.startDebugging(launch, clonedConfig || name, { noDebug: debugStartOptions && debugStartOptions.noDebug }); + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: DEBUG_RUN_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.F5, + mac: { primary: KeyMod.WinCtrl | KeyCode.F5 }, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + handler: async (accessor: ServicesAccessor) => { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(DEBUG_START_COMMAND_ID, { noDebug: true }); + } + }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.toggleBreakpoint', weight: KeybindingWeight.WorkbenchContrib + 5, @@ -389,22 +449,27 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.renameWatchExpression', + id: EDIT_EXPRESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib + 5, when: CONTEXT_WATCH_EXPRESSIONS_FOCUSED, primary: KeyCode.F2, mac: { primary: KeyCode.Enter }, - handler: (accessor) => { - const listService = accessor.get(IListService); + handler: (accessor: ServicesAccessor, expression: Expression | unknown) => { const debugService = accessor.get(IDebugService); - const focused = listService.lastFocusedList; - - if (focused) { - const elements = focused.getFocus(); - if (Array.isArray(elements) && elements[0] instanceof Expression) { - debugService.getViewModel().setSelectedExpression(elements[0]); + if (!(expression instanceof Expression)) { + const listService = accessor.get(IListService); + const focused = listService.lastFocusedList; + if (focused) { + const elements = focused.getFocus(); + if (Array.isArray(elements) && elements[0] instanceof Expression) { + expression = elements[0]; + } } } + + if (expression instanceof Expression) { + debugService.getViewModel().setSelectedExpression(expression); + } } }); @@ -429,16 +494,21 @@ export function registerCommands(): void { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'debug.removeWatchExpression', + id: REMOVE_EXPRESSION_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_EXPRESSION_SELECTED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, - handler: (accessor) => { - const listService = accessor.get(IListService); + handler: (accessor: ServicesAccessor, expression: Expression | unknown) => { const debugService = accessor.get(IDebugService); - const focused = listService.lastFocusedList; + if (expression instanceof Expression) { + debugService.removeWatchExpressions(expression.getId()); + return; + } + + const listService = accessor.get(IListService); + const focused = listService.lastFocusedList; if (focused) { let elements = focused.getFocus(); if (Array.isArray(elements) && elements[0] instanceof Expression) { @@ -455,7 +525,7 @@ export function registerCommands(): void { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'debug.removeBreakpoint', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_SELECTED.toNegated()), + when: ContextKeyExpr.and(CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED.toNegated()), primary: KeyCode.Delete, mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, handler: (accessor) => { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index f3cc95b118..627076a56f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, EditorAction, IActionOptions, EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -24,24 +24,35 @@ import { Position } from 'vs/editor/common/core/position'; import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { raceTimeout } from 'vs/base/common/async'; +import { registerAction2, MenuId } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; -class ToggleBreakpointAction extends EditorAction { +class ToggleBreakpointAction extends EditorAction2 { constructor() { super({ - id: TOGGLE_BREAKPOINT_ID, - label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), - alias: 'Debug: Toggle Breakpoint', + id: 'editor.debug.action.toggleBreakpoint', + title: { + value: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), + original: 'Toggle Breakpoint', + mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, - kbOpts: { - kbExpr: EditorContextKeys.editorTextFocus, + keybinding: { + when: EditorContextKeys.editorTextFocus, primary: KeyCode.F9, weight: KeybindingWeight.EditorContrib + }, + menu: { + when: CONTEXT_DEBUGGERS_AVAILABLE, + id: MenuId.MenubarDebugMenu, + group: '4_new_breakpoint', + order: 1 } }); } - async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): Promise { if (editor.hasModel()) { const debugService = accessor.get(IDebugService); const modelUri = editor.getModel().uri; @@ -49,33 +60,39 @@ class ToggleBreakpointAction extends EditorAction { // Does not account for multi line selections, Set to remove multiple cursor on the same line const lineNumbers = [...new Set(editor.getSelections().map(s => s.getPosition().lineNumber))]; - return Promise.all(lineNumbers.map(line => { + await Promise.all(lineNumbers.map(async line => { const bps = debugService.getModel().getBreakpoints({ lineNumber: line, uri: modelUri }); if (bps.length) { - return Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); + await Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); } else if (canSet) { - return (debugService.addBreakpoints(modelUri, [{ lineNumber: line }])); - } else { - return []; + await debugService.addBreakpoints(modelUri, [{ lineNumber: line }]); } })); } } } -export const TOGGLE_CONDITIONAL_BREAKPOINT_ID = 'editor.debug.action.conditionalBreakpoint'; -class ConditionalBreakpointAction extends EditorAction { - +class ConditionalBreakpointAction extends EditorAction2 { constructor() { super({ - id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, - label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), - alias: 'Debug: Add Conditional Breakpoint...', - precondition: CONTEXT_DEBUGGERS_AVAILABLE + id: 'editor.debug.action.conditionalBreakpoint', + title: { + value: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), + original: 'Debug: Add Conditional Breakpoint...', + mnemonicTitle: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") + }, + f1: true, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + menu: { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { const debugService = accessor.get(IDebugService); const position = editor.getPosition(); @@ -85,19 +102,28 @@ class ConditionalBreakpointAction extends EditorAction { } } -export const ADD_LOG_POINT_ID = 'editor.debug.action.addLogPoint'; -class LogPointAction extends EditorAction { +class LogPointAction extends EditorAction2 { constructor() { super({ - id: ADD_LOG_POINT_ID, - label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), - alias: 'Debug: Add Logpoint...', - precondition: CONTEXT_DEBUGGERS_AVAILABLE + id: 'editor.debug.action.addLogPoint', + title: { + value: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), + original: 'Debug: Add Logpoint...', + mnemonicTitle: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") + }, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + f1: true, + menu: { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE + } }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): void { const debugService = accessor.get(IDebugService); const position = editor.getPosition(); @@ -476,9 +502,9 @@ class CloseExceptionWidgetAction extends EditorAction { } export function registerEditorActions(): void { - registerEditorAction(ToggleBreakpointAction); - registerEditorAction(ConditionalBreakpointAction); - registerEditorAction(LogPointAction); + registerAction2(ToggleBreakpointAction); + registerAction2(ConditionalBreakpointAction); + registerAction2(LogPointAction); registerEditorAction(RunToCursorAction); registerEditorAction(StepIntoTargetsAction); registerEditorAction(SelectionToReplAction); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 28b1dee90f..bdf2f9a62f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -23,7 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugEditorContribution, IDebugService, State, IStackFrame, IDebugConfiguration, IExpression, IExceptionInfo, IDebugSession, CONTEXT_EXCEPTION_WIDGET_VISIBLE } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionWidget } from 'vs/workbench/contrib/debug/browser/exceptionWidget'; -import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { Position } from 'vs/editor/common/core/position'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { memoize, createMemoizer } from 'vs/base/common/decorators'; @@ -339,7 +339,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (this.hoverRange) { this.showHover(this.hoverRange, false); } - }, hoverOption.delay); + }, hoverOption.delay * 2); this.toDispose.push(scheduler); return scheduler; @@ -432,7 +432,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { this.closeExceptionWidget(); } else if (sameUri) { const exceptionInfo = await focusedSf.thread.exceptionInfo; - if (exceptionInfo && exceptionSf.range.startLineNumber && exceptionSf.range.startColumn) { + if (exceptionInfo) { this.showExceptionWidget(exceptionInfo, this.debugService.getViewModel().focusedSession, exceptionSf.range.startLineNumber, exceptionSf.range.startColumn); } } @@ -452,9 +452,13 @@ export class DebugEditorContribution implements IDebugEditorContribution { closeExceptionWidget(): void { if (this.exceptionWidget) { + const shouldFocusEditor = this.exceptionWidget.hasfocus(); this.exceptionWidget.dispose(); this.exceptionWidget = undefined; this.exceptionWidgetVisible.set(false); + if (shouldFocusEditor) { + this.editor.focus(); + } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 0545a61e8f..2c23bd69e6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -105,7 +105,7 @@ export class DebugHoverWidget implements IContentWidget { this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree')); this.treeContainer.setAttribute('role', 'tree'); const tip = dom.append(this.complexValueContainer, $('.tip')); - tip.textContent = nls.localize('quickTip', 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); + tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); const dataSource = new DebugHoverDataSource(); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [this.instantiationService.createInstance(VariablesRenderer)], diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index 41ebfcd52a..c8222267ad 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -15,25 +15,37 @@ export const callStackViewIcon = registerIcon('callstack-view-icon', Codicon.deb export const breakpointsViewIcon = registerIcon('breakpoints-view-icon', Codicon.debugAlt, localize('breakpointsViewIcon', 'View icon of the breakpoints view.')); export const loadedScriptsViewIcon = registerIcon('loaded-scripts-view-icon', Codicon.debugAlt, localize('loadedScriptsViewIcon', 'View icon of the loaded scripts view.')); -export const debugBreakpoint = registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')); -export const debugBreakpointDisabled = registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')); -export const debugBreakpointUnverified = registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')); -export const debugBreakpointHint = registerIcon('debug-hint', Codicon.debugHint, localize('debugBreakpointHint', 'Icon for breakpoint hints shown on hover in editor glyph margin.')); -export const debugBreakpointFunction = registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')); -export const debugBreakpointFunctionUnverified = registerIcon('debug-breakpoint-function-unverified', Codicon.debugBreakpointFunctionUnverified, localize('debugBreakpointFunctionUnverified', 'Icon for unverified function breakpoints.')); -export const debugBreakpointFunctionDisabled = registerIcon('debug-breakpoint-function-disabled', Codicon.debugBreakpointFunctionDisabled, localize('debugBreakpointFunctionDisabled', 'Icon for disabled function breakpoints.')); +export const breakpoint = { + regular: registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')), + disabled: registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')), + unverified: registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')) +}; +export const functionBreakpoint = { + regular: registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')), + disabled: registerIcon('debug-breakpoint-function-disabled', Codicon.debugBreakpointFunctionDisabled, localize('debugBreakpointFunctionDisabled', 'Icon for disabled function breakpoints.')), + unverified: registerIcon('debug-breakpoint-function-unverified', Codicon.debugBreakpointFunctionUnverified, localize('debugBreakpointFunctionUnverified', 'Icon for unverified function breakpoints.')) +}; +export const conditionalBreakpoint = { + regular: registerIcon('debug-breakpoint-conditional', Codicon.debugBreakpointConditional, localize('debugBreakpointConditional', 'Icon for conditional breakpoints.')), + disabled: registerIcon('debug-breakpoint-conditional-disabled', Codicon.debugBreakpointConditionalDisabled, localize('debugBreakpointConditionalDisabled', 'Icon for disabled conditional breakpoints.')), + unverified: registerIcon('debug-breakpoint-conditional-unverified', Codicon.debugBreakpointConditionalUnverified, localize('debugBreakpointConditionalUnverified', 'Icon for unverified conditional breakpoints.')) +}; +export const dataBreakpoint = { + regular: registerIcon('debug-breakpoint-data', Codicon.debugBreakpointData, localize('debugBreakpointData', 'Icon for data breakpoints.')), + disabled: registerIcon('debug-breakpoint-data-disabled', Codicon.debugBreakpointDataDisabled, localize('debugBreakpointDataDisabled', 'Icon for disabled data breakpoints.')), + unverified: registerIcon('debug-breakpoint-data-unverified', Codicon.debugBreakpointDataUnverified, localize('debugBreakpointDataUnverified', 'Icon for unverified data breakpoints.')), +}; +export const logBreakpoint = { + regular: registerIcon('debug-breakpoint-log', Codicon.debugBreakpointLog, localize('debugBreakpointLog', 'Icon for log breakpoints.')), + disabled: registerIcon('debug-breakpoint-log-disabled', Codicon.debugBreakpointLogDisabled, localize('debugBreakpointLogDisabled', 'Icon for disabled log breakpoint.')), + unverified: registerIcon('debug-breakpoint-log-unverified', Codicon.debugBreakpointLogUnverified, localize('debugBreakpointLogUnverified', 'Icon for unverified log breakpoints.')), +}; +export const debugBreakpointHint = registerIcon('debug-hint', Codicon.debugHint, localize('debugBreakpointHint', 'Icon for breakpoint hints shown on hover in editor glyph margin.')); export const debugBreakpointUnsupported = registerIcon('debug-breakpoint-unsupported', Codicon.debugBreakpointUnsupported, localize('debugBreakpointUnsupported', 'Icon for unsupported breakpoints.')); -export const debugBreakpointConditionalUnverified = registerIcon('debug-breakpoint-conditional-unverified', Codicon.debugBreakpointConditionalUnverified, localize('debugBreakpointConditionalUnverified', 'Icon for unverified conditional breakpoints.')); -export const debugBreakpointConditional = registerIcon('debug-breakpoint-conditional', Codicon.debugBreakpointConditional, localize('debugBreakpointConditional', 'Icon for conditional breakpoints.')); -export const debugBreakpointConditionalDisabled = registerIcon('debug-breakpoint-conditional-disabled', Codicon.debugBreakpointConditionalDisabled, localize('debugBreakpointConditionalDisabled', 'Icon for disabled conditional breakpoints.')); -export const debugBreakpointDataUnverified = registerIcon('debug-breakpoint-data-unverified', Codicon.debugBreakpointDataUnverified, localize('debugBreakpointDataUnverified', 'Icon for unverified data breakpoints.')); -export const debugBreakpointData = registerIcon('debug-breakpoint-data', Codicon.debugBreakpointData, localize('debugBreakpointData', 'Icon for data breakpoints.')); -export const debugBreakpointDataDisabled = registerIcon('debug-breakpoint-data-disabled', Codicon.debugBreakpointDataDisabled, localize('debugBreakpointDataDisabled', 'Icon for disabled data breakpoints.')); -export const debugBreakpointLogUnverified = registerIcon('debug-breakpoint-log-unverified', Codicon.debugBreakpointLogUnverified, localize('debugBreakpointLogUnverified', 'Icon for unverified log breakpoints.')); -export const debugBreakpointLog = registerIcon('debug-breakpoint-log', Codicon.debugBreakpointLog, localize('debugBreakpointLog', 'Icon for log breakpoints.')); -export const debugBreakpointLogDisabled = registerIcon('debug-breakpoint-log-disabled', Codicon.debugBreakpointLogDisabled, localize('debugBreakpointLogDisabled', 'Icon for disabled log breakpoint.')); +export const allBreakpoints = [breakpoint, functionBreakpoint, conditionalBreakpoint, dataBreakpoint, logBreakpoint]; + export const debugStackframe = registerIcon('debug-stackframe', Codicon.debugStackframe, localize('debugStackframe', 'Icon for a stackframe shown in the editor glyph margin.')); export const debugStackframeFocused = registerIcon('debug-stackframe-focused', Codicon.debugStackframeFocused, localize('debugStackframeFocused', 'Icon for a focused stackframe shown in the editor glyph margin.')); @@ -60,11 +72,11 @@ export const debugConsole = registerIcon('debug-console', Codicon.gear, localize export const debugCollapseAll = registerIcon('debug-collapse-all', Codicon.collapseAll, localize('debugCollapseAll', 'Icon for the collapse all action in the debug views.')); export const callstackViewSession = registerIcon('callstack-view-session', Codicon.bug, localize('callstackViewSession', 'Icon for the session icon in the call stack view.')); export const debugConsoleClearAll = registerIcon('debug-console-clear-all', Codicon.clearAll, localize('debugConsoleClearAll', 'Icon for the clear all action in the debug console.')); -export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove-all', Codicon.closeAll, localize('watchExpressionsRemoveAll', 'Icon for the remove all action in the watch view.')); +export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove-all', Codicon.closeAll, localize('watchExpressionsRemoveAll', 'Icon for the Remove All action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); -export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the remove all action in the breakpoints view.')); +export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the Remove All action in the breakpoints view.')); export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon.activateBreakpoints, localize('breakpointsActivate', 'Icon for the activate action in the breakpoints view.')); export const debugConsoleEvaluationInput = registerIcon('debug-console-evaluation-input', Codicon.arrowSmallRight, localize('debugConsoleEvaluationInput', 'Icon for the debug evaluation input marker.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index 4d01c555ff..ca49c2f6f8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -115,7 +115,8 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { const pick = await provider.pick(); if (pick) { - await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: pick.config.type }); + // Use the type of the provider, not of the config since config sometimes have subtypes (for example "node-terminal") + await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: provider.type }); this.debugService.startDebugging(pick.launch, pick.config); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 447f59dc2d..9796c0d066 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -17,7 +17,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { DebugModel, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; -import * as debugactions from 'vs/workbench/contrib/debug/browser/debugActions'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -25,7 +24,6 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { parse, getFirstFrame } from 'vs/base/common/console'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAction, Action } from 'vs/base/common/actions'; @@ -48,9 +46,9 @@ import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { ITextModel } from 'vs/editor/common/model'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -96,8 +94,7 @@ export class DebugService implements IDebugService { @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IQuickInputService private readonly quickInputService: IQuickInputService ) { this.toDispose = []; @@ -111,17 +108,17 @@ export class DebugService implements IDebugService { this.adapterManager = this.instantiationService.createInstance(AdapterManager); this.configurationManager = this.instantiationService.createInstance(ConfigurationManager, this.adapterManager); this.toDispose.push(this.configurationManager); + this.debugStorage = this.instantiationService.createInstance(DebugStorage); contextKeyService.bufferChangeEvents(() => { this.debugType = CONTEXT_DEBUG_TYPE.bindTo(contextKeyService); this.debugState = CONTEXT_DEBUG_STATE.bindTo(contextKeyService); this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.debugUx = CONTEXT_DEBUG_UX.bindTo(contextKeyService); - this.debugUx.set((this.adapterManager.hasDebuggers() && !!this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); + this.debugUx.set(this.debugStorage.loadDebugUxState()); this.breakpointsExist = CONTEXT_BREAKPOINTS_EXIST.bindTo(contextKeyService); }); - this.debugStorage = this.instantiationService.createInstance(DebugStorage); this.model = this.instantiationService.createInstance(DebugModel, this.debugStorage); this.telemetry = this.instantiationService.createInstance(DebugTelemetry, this.model); const setBreakpointsExistContext = () => this.breakpointsExist.set(!!(this.model.getBreakpoints().length || this.model.getDataBreakpoints().length || this.model.getFunctionBreakpoints().length)); @@ -149,16 +146,6 @@ export class DebugService implements IDebugService { session.disconnect(); } })); - this.toDispose.push(this.extensionHostDebugService.onLogToSession(event => { - const session = this.model.getSession(event.sessionId, true); - if (session) { - // extension logged output -> show it in REPL - const sev = event.log.severity === 'warn' ? severity.Warning : event.log.severity === 'error' ? severity.Error : severity.Info; - const { args, stack } = parse(event.log); - const frame = !!stack ? getFirstFrame(stack) : undefined; - session.logToRepl(sev, args, frame); - } - })); this.toDispose.push(this.viewModel.onDidFocusStackFrame(() => { this.onStateChange(); @@ -167,7 +154,9 @@ export class DebugService implements IDebugService { this.onStateChange(); })); this.toDispose.push(Event.any(this.adapterManager.onDidRegisterDebugger, this.configurationManager.onDidSelectConfiguration)(() => { - this.debugUx.set(!!(this.state !== State.Inactive || (this.configurationManager.selectedConfiguration.name && this.adapterManager.hasDebuggers())) ? 'default' : 'simple'); + const debugUxValue = !!(this.state !== State.Inactive || (this.configurationManager.selectedConfiguration.name && this.adapterManager.hasDebuggers())) ? 'default' : 'simple'; + this.debugUx.set(debugUxValue); + this.debugStorage.storeDebugUxState(debugUxValue); })); this.toDispose.push(this.model.onDidChangeCallStack(() => { const numberOfSessions = this.model.getSessions().filter(s => !s.parentSession).length; @@ -253,7 +242,9 @@ export class DebugService implements IDebugService { this.debugState.set(getStateLabel(state)); this.inDebugMode.set(state !== State.Inactive); // Only show the simple ux if debug is not yet started and if no launch.json exists - this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || (this.adapterManager.hasDebuggers() && this.configurationManager.selectedConfiguration.name)) ? 'default' : 'simple'); + const debugUxValue = ((state !== State.Inactive && state !== State.Initializing) || (this.adapterManager.hasDebuggers() && this.configurationManager.selectedConfiguration.name)) ? 'default' : 'simple'; + this.debugUx.set(debugUxValue); + this.debugStorage.storeDebugUxState(debugUxValue); }); this.previousState = state; this._onDidChangeState.fire(state); @@ -290,6 +281,11 @@ export class DebugService implements IDebugService { await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { await this.editorService.saveAll(); + const activeEditor = this.editorService.activeEditorPane; + if (activeEditor) { + // Make sure to save the active editor in case it is in untitled file it wont be saved as part of saveAll #111850 + await this.editorService.save({ editor: activeEditor.input, groupId: activeEditor.group.id }); + } } await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); @@ -302,15 +298,6 @@ export class DebugService implements IDebugService { if (typeof configOrName === 'string' && launch) { config = launch.getConfiguration(configOrName); compound = launch.getCompound(configOrName); - - const sessions = this.model.getSessions(); - const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); - if (sessions.some(s => (s.configuration.name === configOrName && s.root === launch.workspace) && (!launch || !launch.workspace || !s.root || this.uriIdentityService.extUri.isEqual(s.root.uri, launch.workspace.uri)))) { - throw new Error(alreadyRunningMessage); - } - if (compound && compound.configurations && sessions.some(p => compound!.configurations.indexOf(p.configuration.name) !== -1)) { - throw new Error(alreadyRunningMessage); - } } else if (typeof configOrName !== 'string') { config = configOrName; } @@ -784,7 +771,7 @@ export class DebugService implements IDebugService { } private async showError(message: string, errorActions: ReadonlyArray = []): Promise { - const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); + const configureAction = new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID)); const actions = [...errorActions, configureAction]; const { choice } = await this.dialogService.show(severity.Error, message, actions.map(a => a.label).concat(nls.localize('cancel', "Cancel")), { cancelId: actions.length }); if (choice < actions.length) { @@ -917,12 +904,11 @@ export class DebugService implements IDebugService { } addFunctionBreakpoint(name?: string, id?: string): void { - const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); - this.viewModel.setSelectedBreakpoint(newFunctionBreakpoint); + this.model.addFunctionBreakpoint(name || '', id); } - async renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { - this.model.renameFunctionBreakpoint(id, newFunctionName); + async updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise { + this.model.updateFunctionBreakpoint(id, update); this.debugStorage.storeBreakpoints(this.model); await this.sendFunctionBreakpoints(); } @@ -946,6 +932,11 @@ export class DebugService implements IDebugService { await this.sendDataBreakpoints(); } + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + this.model.setExceptionBreakpoints(data); + this.debugStorage.storeBreakpoints(this.model); + } + async setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): Promise { this.model.setExceptionBreakpointCondition(exceptionBreakpoint, condition); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index aed646bbc5..989cc485cf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -64,7 +64,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidChangeREPLElements = new Emitter(); - private name: string | undefined; + private _name: string | undefined; private readonly _onDidChangeName = new Emitter(); constructor( @@ -88,7 +88,7 @@ export class DebugSession implements IDebugSession { ) { this._options = options || {}; if (this.hasSeparateRepl()) { - this.repl = new ReplModel(); + this.repl = new ReplModel(this.configurationService); } else { this.repl = (this.parentSession as DebugSession).repl; } @@ -146,15 +146,18 @@ export class DebugSession implements IDebugSession { getLabel(): string { const includeRoot = this.workspaceContextService.getWorkspace().folders.length > 1; - const name = this.name || this.configuration.name; - return includeRoot && this.root ? `${name} (${resources.basenameOrAuthority(this.root.uri)})` : name; + return includeRoot && this.root ? `${this.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.name; } setName(name: string): void { - this.name = name; + this._name = name; this._onDidChangeName.fire(name); } + get name(): string { + return this._name || this.configuration.name; + } + get state(): State { if (!this.initialized) { return State.Initializing; @@ -232,7 +235,7 @@ export class DebugSession implements IDebugSession { try { const customTelemetryService = await dbgr.getCustomTelemetryService(); const debugAdapter = await dbgr.createDebugAdapter(this); - this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.extensionHostDebugService, this.openerService, this.notificationService); + this.raw = new RawDebugSession(debugAdapter, dbgr, this.id, this.telemetryService, customTelemetryService, this.extensionHostDebugService, this.openerService, this.notificationService); await this.raw.start(); this.registerListeners(); @@ -253,7 +256,7 @@ export class DebugSession implements IDebugSession { this.initialized = true; this._onDidChangeState.fire(); - this.model.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); + this.debugService.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); } catch (err) { this.initialized = true; this._onDidChangeState.fire(); @@ -837,7 +840,8 @@ export class DebugSession implements IDebugSession { await promises.topCallStack; focus(); await promises.wholeCallStack; - if (!this.debugService.getViewModel().focusedStackFrame) { + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + if (!focusedStackFrame || !focusedStackFrame.source || focusedStackFrame.source.presentationHint === 'deemphasize') { // The top stack frame can be deemphesized so try to focus again #68616 focus(); } @@ -1015,8 +1019,8 @@ export class DebugSession implements IDebugSession { this._onDidProgressEnd.fire(event); })); this.rawListeners.push(this.raw.onDidInvalidated(async event => { - if (!(event.body.areas && event.body.areas.length === 1 && event.body.areas[0] === 'variables')) { - // If invalidated event only requires to update variables, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 + if (!(event.body.areas && event.body.areas.length === 1 && (event.body.areas[0] === 'variables' || event.body.areas[0] === 'watch'))) { + // If invalidated event only requires to update variables or watch, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 this.cancelAllRequests(); this.model.clearThreads(this.getId(), true); await this.fetchThreads(this.stoppedDetails); diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 5cde5219dc..d5489b29cc 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -12,7 +12,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IAction } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; @@ -109,8 +109,9 @@ export class DebugTaskRunner { await this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); return Promise.resolve(TaskRunResult.Failure); } catch (err) { - await onError(err.message, [this.taskService.configureAction()]); - return TaskRunResult.Failure; + let debugAnyway = false; + await onError(err.message, [new Action('debug.debugAnyway', nls.localize('debugAnyway', "Debug Anyway"), undefined, true, async () => { debugAnyway = true; }), this.taskService.configureAction()]); + return debugAnyway ? TaskRunResult.Success : TaskRunResult.Failure; } } @@ -169,17 +170,23 @@ export class DebugTaskRunner { return taskPromise.then(withUndefinedAsNull); }); - return new Promise((c, e) => { + return new Promise(async (c, e) => { + const waitForInput = new Promise(resolve => once(e => (e.kind === TaskEventKind.AcquiredInput) && e.taskId === task._id, this.taskService.onDidStateChange)(() => { + resolve(); + })); + promise.then(result => { taskStarted = true; c(result); }, error => e(error)); + await waitForInput; + setTimeout(() => { if (!taskStarted) { const errorMessage = typeof taskId === 'string' - ? nls.localize('taskNotTrackedWithTaskId', "The specified task cannot be tracked.") - : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked.", JSON.stringify(taskId)); + ? nls.localize('taskNotTrackedWithTaskId', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", taskId) + : nls.localize('taskNotTracked', "The task '{0}' cannot be tracked. Make sure to have a problem matcher defined.", JSON.stringify(taskId)); e({ severity: severity.Error, message: errorMessage }); } }, 10000); diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 127aac1cb6..c351d20f83 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -8,28 +8,30 @@ import * as errors from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, Separator } from 'vs/base/common/actions'; +import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugConfiguration, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfiguration, IDebugService, State, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { registerThemingParticipant, IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { localize } from 'vs/nls'; +import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, ContextKeyExpression, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { debugToolBarBackground, debugToolBarBorder } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { URI } from 'vs/base/common/uri'; +import { CONTINUE_LABEL, CONTINUE_ID, PAUSE_ID, STOP_ID, DISCONNECT_ID, STEP_OVER_ID, STEP_INTO_ID, RESTART_SESSION_ID, STEP_OUT_ID, STEP_BACK_ID, REVERSE_CONTINUE_ID, RESTART_LABEL, STEP_OUT_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, DISCONNECT_LABEL, STOP_LABEL, PAUSE_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; @@ -75,15 +77,10 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.actionBar = this._register(new ActionBar(actionBarContainer, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: (action: IAction) => { - if (action.id === FocusSessionAction.ID) { + if (action.id === FOCUS_SESSION_ID) { return this.instantiationService.createInstance(FocusSessionActionViewItem, action, undefined); - } else if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } - - return undefined; + return createActionViewItem(this.instantiationService, action); } })); @@ -94,7 +91,8 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { return this.hide(); } - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); + const actions: IAction[] = []; + const disposable = createAndFillInActionBarActions(this.debugToolBarMenu, { shouldForwardArgs: true }, actions, () => false); if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id && first.enabled === second.enabled)) { this.actionBar.clear(); this.actionBar.push(actions, { icon: true, label: false }); @@ -115,9 +113,11 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); - this._register(this.debugService.onDidNewSession(() => this.updateScheduler.schedule())); - this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.toolBarLocation')) { + this.updateScheduler.schedule(); + } + })); this._register(this.debugToolBarMenu.onDidChange(() => this.updateScheduler.schedule())); this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { // check for error @@ -225,12 +225,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } - private onDidConfigurationChange(event: IConfigurationChangeEvent): void { - if (event.affectsConfiguration('debug.hideActionBar') || event.affectsConfiguration('debug.toolBarLocation')) { - this.updateScheduler.schedule(); - } - } - private show(): void { if (this.isVisible) { this.setCoordinates(); @@ -251,19 +245,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { dom.hide(this.$el); } - static getActions(menu: IMenu, debugService: IDebugService, instantiationService: IInstantiationService): { actions: IAction[], disposable: IDisposable } { - const actions: IAction[] = []; - const disposable = createAndFillInActionBarActions(menu, undefined, actions, () => false); - if (debugService.getViewModel().isMultiSessionView()) { - actions.push(instantiationService.createInstance(FocusSessionAction, FocusSessionAction.ID, FocusSessionAction.LABEL)); - } - - return { - actions: actions.filter(a => !(a instanceof Separator)), // do not render separators for now - disposable - }; - } - dispose(): void { super.dispose(); @@ -276,127 +257,43 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } -export const debugToolBarBackground = registerColor('debugToolBar.background', { - dark: '#333333', - light: '#F3F3F3', - hc: '#000000' -}, localize('debugToolBarBackground', "Debug toolbar background color.")); +// Debug toolbar -export const debugToolBarBorder = registerColor('debugToolBar.border', { - dark: null, - light: null, - hc: null -}, localize('debugToolBarBorder', "Debug toolbar border color.")); +const registerDebugToolBarItem = (id: string, title: string, order: number, icon?: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { + MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { + group: 'navigation', + when, + order, + command: { + id, + title, + icon, + precondition + } + }); -export const debugIconStartForeground = registerColor('debugIcon.startForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); + // Register actions in debug viewlet when toolbar is docked + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + group: 'navigation', + when: ContextKeyExpr.and(when, ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')), + order, + command: { + id, + title, + icon, + precondition + } + }); +}; -export const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); - -export const debugIconStopForeground = registerColor('debugIcon.stopForeground', { - dark: '#F48771', - light: '#A1260D', - hc: '#F48771' -}, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); - -export const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { - dark: '#F48771', - light: '#A1260D', - hc: '#F48771' -}, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); - -export const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); - -export const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); - -export const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); - -export const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); - -export const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); - -export const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); - -registerThemingParticipant((theme, collector) => { - - const debugIconStartColor = theme.getColor(debugIconStartForeground); - if (debugIconStartColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStart)} { color: ${debugIconStartColor} !important; }`); - } - - const debugIconPauseColor = theme.getColor(debugIconPauseForeground); - if (debugIconPauseColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugPause)} { color: ${debugIconPauseColor} !important; }`); - } - - const debugIconStopColor = theme.getColor(debugIconStopForeground); - if (debugIconStopColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStop)} { color: ${debugIconStopColor} !important; }`); - } - - const debugIconDisconnectColor = theme.getColor(debugIconDisconnectForeground); - if (debugIconDisconnectColor) { - collector.addRule(`.monaco-workbench .debug-view-content ${ThemeIcon.asCSSSelector(icons.debugDisconnect)}, .monaco-workbench .debug-toolbar ${ThemeIcon.asCSSSelector(icons.debugDisconnect)} { color: ${debugIconDisconnectColor} !important; }`); - } - - const debugIconRestartColor = theme.getColor(debugIconRestartForeground); - if (debugIconRestartColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestart)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugRestartFrame)} { color: ${debugIconRestartColor} !important; }`); - } - - const debugIconStepOverColor = theme.getColor(debugIconStepOverForeground); - if (debugIconStepOverColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOver)} { color: ${debugIconStepOverColor} !important; }`); - } - - const debugIconStepIntoColor = theme.getColor(debugIconStepIntoForeground); - if (debugIconStepIntoColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepInto)} { color: ${debugIconStepIntoColor} !important; }`); - } - - const debugIconStepOutColor = theme.getColor(debugIconStepOutForeground); - if (debugIconStepOutColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepOut)} { color: ${debugIconStepOutColor} !important; }`); - } - - const debugIconContinueColor = theme.getColor(debugIconContinueForeground); - if (debugIconContinueColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugContinue)}, .monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugReverseContinue)} { color: ${debugIconContinueColor} !important; }`); - } - - const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); - if (debugIconStepBackColor) { - collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icons.debugStepBack)} { color: ${debugIconStepBackColor} !important; }`); - } -}); +registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); +registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); +registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH); +registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart); +registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 60, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, undefined, CONTEXT_MULTI_SESSION_DEBUG); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 926777c6e4..70e2ff2b8b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -6,34 +6,30 @@ import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; import { IAction, IActionViewItem } from 'vs/base/common/actions'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID, CONTEXT_DEBUG_STATE, ILaunch, getStateLabel, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { memoize } from 'vs/base/common/decorators'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IMenu, MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyDefinedExpr } from 'vs/platform/contextkey/common/contextkey'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; -import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { debugConsole } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { debugConfigure } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { FOCUS_SESSION_ID, SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -41,9 +37,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private progressResolve: (() => void) | undefined; private breakpointView: ViewPane | undefined; private paneListeners = new Map(); - private debugToolBarMenu: IMenu | undefined; - private disposeOnTitleUpdate: IDisposable | undefined; - private updateToolBarScheduler: RunOnceScheduler; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -58,22 +51,13 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IContextViewService private readonly contextViewService: IContextViewService, - @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); - this.updateToolBarScheduler = this._register(new RunOnceScheduler(() => { - if (this.configurationService.getValue('debug').toolBarLocation === 'docked') { - this.updateTitleArea(); - } - }, 20)); - // When there are potential updates to the docked debug toolbar we need to update it this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); - this._register(this.debugService.onDidNewSession(() => this.updateToolBarScheduler.schedule())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateToolBarScheduler.schedule())); this._register(this.contextKeyService.onDidChangeContext(e => { if (e.affectsSome(new Set([CONTEXT_DEBUG_UX_KEY]))) { @@ -104,83 +88,15 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } - @memoize - private get startAction(): StartAction { - return this._register(this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL)); - } - - @memoize - private get configureAction(): ConfigureAction { - return this._register(this.instantiationService.createInstance(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL)); - } - - @memoize - private get toggleReplAction(): OpenDebugConsoleAction { - return this._register(this.instantiationService.createInstance(OpenDebugConsoleAction, OpenDebugConsoleAction.ID, OpenDebugConsoleAction.LABEL)); - } - - @memoize - private get selectAndStartAction(): SelectAndStartAction { - return this._register(this.instantiationService.createInstance(SelectAndStartAction, SelectAndStartAction.ID, nls.localize('startAdditionalSession', "Start Additional Session"))); - } - - getActions(): IAction[] { - if (CONTEXT_DEBUG_UX.getValue(this.contextKeyService) === 'simple') { - return []; - } - - if (!this.showInitialDebugActions) { - - if (!this.debugToolBarMenu) { - this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); - this._register(this.debugToolBarMenu); - this._register(this.debugToolBarMenu.onDidChange(() => this.updateToolBarScheduler.schedule())); - } - - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); - if (this.disposeOnTitleUpdate) { - dispose(this.disposeOnTitleUpdate); - } - this.disposeOnTitleUpdate = disposable; - - return actions; - } - - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - return [this.toggleReplAction]; - } - - return [this.startAction, this.configureAction, this.toggleReplAction]; - } - - get showInitialDebugActions(): boolean { - const state = this.debugService.state; - return state === State.Inactive || this.configurationService.getValue('debug').toolBarLocation !== 'docked'; - } - - getSecondaryActions(): IAction[] { - if (this.showInitialDebugActions) { - return []; - } - - return [this.selectAndStartAction, this.configureAction, this.toggleReplAction]; - } - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === StartAction.ID) { + if (action.id === DEBUG_START_COMMAND_ID) { this.startDebugActionViewItem = this.instantiationService.createInstance(StartDebugActionViewItem, null, action); return this.startDebugActionViewItem; } - if (action.id === FocusSessionAction.ID) { + if (action.id === FOCUS_SESSION_ID) { return new FocusSessionActionViewItem(action, undefined, this.debugService, this.themeService, this.contextViewService, this.configurationService); } - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(MenuEntryActionViewItem, action); - } else if (action instanceof SubmenuItemAction) { - return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); - } - - return undefined; + return createActionViewItem(this.instantiationService, action); } focusView(id: string): void { @@ -201,8 +117,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { return new Promise(resolve => this.progressResolve = resolve); }); } - - this.updateToolBarScheduler.schedule(); } addPanes(panes: { pane: ViewPane, size: number, index?: number }[]): void { @@ -236,33 +150,110 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } -export class OpenDebugConsoleAction extends ToggleViewAction { - public static readonly ID = 'workbench.debug.action.toggleRepl'; - public static readonly LABEL = nls.localize('toggleDebugPanel', "Debug Console"); - - constructor( - id: string, - label: string, - @IViewsService viewsService: IViewsService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, REPL_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService, ThemeIcon.asClassName(debugConsole)); +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_UX.notEqualsTo('simple'), WorkbenchStateContext.notEqualsTo('empty'), + ContextKeyExpr.or(CONTEXT_DEBUG_STATE.isEqualTo('inactive'), ContextKeyExpr.notEquals('config.debug.toolBarLocation', 'docked'))), + order: 10, + group: 'navigation', + command: { + precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)), + id: DEBUG_START_COMMAND_ID, + title: DEBUG_START_LABEL } -} +}); -export class OpenDebugViewletAction extends ShowViewletAction { - public static readonly ID = VIEWLET_ID; - public static readonly LABEL = nls.localize('toggleDebugViewlet', "Show Run and Debug"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); +registerAction2(class extends Action2 { + constructor() { + super({ + id: DEBUG_CONFIGURE_COMMAND_ID, + title: { + value: DEBUG_CONFIGURE_LABEL, + original: DEBUG_CONFIGURE_LABEL, + mnemonicTitle: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + f1: true, + icon: debugConfigure, + precondition: CONTEXT_DEBUG_UX.notEqualsTo('simple'), + menu: [{ + id: MenuId.ViewContainerTitle, + group: 'navigation', + order: 20, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_UX.notEqualsTo('simple'), WorkbenchStateContext.notEqualsTo('empty'), + ContextKeyExpr.or(CONTEXT_DEBUG_STATE.isEqualTo('inactive'), ContextKeyExpr.notEquals('config.debug.toolBarLocation', 'docked'))) + }, { + id: MenuId.ViewContainerTitle, + order: 20, + // Show in debug viewlet secondary actions when debugging and debug toolbar is docked + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')) + }, { + id: MenuId.MenubarDebugMenu, + group: '2_configuration', + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE + }] + }); } -} + + async run(accessor: ServicesAccessor): Promise { + const debugService = accessor.get(IDebugService); + const quickInputService = accessor.get(IQuickInputService); + const configurationManager = debugService.getConfigurationManager(); + let launch: ILaunch | undefined; + if (configurationManager.selectedConfiguration.name) { + launch = configurationManager.selectedConfiguration.launch; + } else { + const launches = configurationManager.getLaunches().filter(l => !l.hidden); + if (launches.length === 1) { + launch = launches[0]; + } else { + const picks = launches.map(l => ({ label: l.name, launch: l })); + const picked = await quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { + activeItem: picks[0], + placeHolder: nls.localize({ key: 'selectWorkspaceFolder', comment: ['User picks a workspace folder or a workspace configuration file here. Workspace configuration files can contain settings and thus a launch.json configuration can be written into one.'] }, "Select a workspace folder to create a launch.json file in or add it to the workspace config file") + }); + if (picked) { + launch = picked.launch; + } + } + } + + if (launch) { + await launch.openConfigFile(false); + } + } +}); + + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'debug.toggleReplIgnoreFocus', + title: nls.localize('debugPanel', "Debug Console"), + toggled: ContextKeyDefinedExpr.create(`view.${REPL_VIEW_ID}.visible`), + menu: [{ + id: ViewsSubMenu, + group: '3_toggleRepl', + order: 30, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID)) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + if (viewsService.isViewVisible(REPL_VIEW_ID)) { + viewsService.closeView(REPL_VIEW_ID); + } else { + await viewsService.openView(REPL_VIEW_ID); + } + } +}); + +MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')), + order: 10, + command: { + id: SELECT_AND_START_ID, + title: nls.localize('startAdditionalSession', "Start Additional Session"), + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts index fc368c12a6..f8ac93313b 100644 --- a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts @@ -120,4 +120,8 @@ export class ExceptionWidget extends ZoneWidget { // Focus into the container for accessibility purposes so the exception and stack trace gets read this.container?.focus(); } + + hasfocus(): boolean { + return dom.isAncestor(document.activeElement, this.container); + } } diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 31b8cccf03..ff3a116e97 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -3,16 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import * as osPath from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); @@ -97,8 +99,28 @@ export class LinkDetector { private createWebLink(url: string): Node { const link = this.createLink(url); - const uri = URI.parse(url); - this.decorateLink(link, () => this.openerService.open(uri, { allowTunneling: !!this.environmentService.remoteAuthority })); + + this.decorateLink(link, async () => { + const uri = URI.parse(url); + + if (uri.scheme === Schemas.file) { + // Just using fsPath here is unsafe: https://github.com/microsoft/vscode/issues/109076 + const fsPath = uri.fsPath; + const path = await this.pathService.path; + const fileUrl = osPath.normalize(((path.sep === osPath.posix.sep) && platform.isWindows) ? fsPath.replace(/\\/g, osPath.posix.sep) : fsPath); + + const resolvedLink = await this.fileService.resolve(URI.parse(fileUrl)); + if (!resolvedLink) { + return; + } + + await this.editorService.openEditor({ resource: resolvedLink.resource, options: { pinned: true } }); + return; + } + + this.openerService.open(url, { allowTunneling: !!this.environmentService.remoteAuthority }); + }); + return link; } @@ -108,14 +130,14 @@ export class LinkDetector { return document.createTextNode(text); } + const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; if (path[0] === '.') { if (!workspaceFolder) { return document.createTextNode(text); } const uri = workspaceFolder.toResource(path); - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; const link = this.createLink(text); - this.decorateLink(link, () => this.editorService.openEditor({ resource: uri, options })); + this.decorateLink(link, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); return link; } @@ -127,13 +149,13 @@ export class LinkDetector { } const link = this.createLink(text); + link.tabIndex = 0; const uri = URI.file(osPath.normalize(path)); this.fileService.resolve(uri).then(stat => { if (stat.isDirectory) { return; } - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; - this.decorateLink(link, () => this.editorService.openEditor({ resource: uri, options })); + this.decorateLink(link, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); }).catch(() => { // If the uri can not be resolved we should not spam the console with error, remain quite #86587 }); @@ -146,9 +168,8 @@ export class LinkDetector { return link; } - private decorateLink(link: HTMLElement, onclick: () => void) { + private decorateLink(link: HTMLElement, onClick: (preserveFocus: boolean) => void) { link.classList.add('link'); - link.title = platform.isMacintosh ? nls.localize('fileLinkMac', "Cmd + click to follow link") : nls.localize('fileLink', "Ctrl + click to follow link"); link.onmousemove = (event) => { link.classList.toggle('pointer', platform.isMacintosh ? event.metaKey : event.ctrlKey); }; link.onmouseleave = () => link.classList.remove('pointer'); link.onclick = (event) => { @@ -156,12 +177,17 @@ export class LinkDetector { if (!selection || selection.type === 'Range') { return; // do not navigate when user is selecting } - if (!(platform.isMacintosh ? event.metaKey : event.ctrlKey)) { - return; - } event.preventDefault(); event.stopImmediatePropagation(); - onclick(); + onClick(false); + }; + link.onkeydown = e => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + event.preventDefault(); + event.stopPropagation(); + onClick(event.keyCode === KeyCode.Space); + } }; } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index b182009325..6b5f6ca09b 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { normalize, isAbsolute, posix } from 'vs/base/common/path'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 901adb253b..763683f722 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -144,22 +144,22 @@ display: none; } -.debug-pane .debug-call-stack .monaco-list-row .monaco-action-bar { +.debug-pane .monaco-list-row .monaco-action-bar { display: none; flex-shrink: 0; margin-right: 1px; } -.debug-pane .debug-call-stack .monaco-list-row:hover .monaco-action-bar { +.debug-pane .monaco-list-row:hover .monaco-action-bar { display: initial; } -.debug-pane .debug-call-stack .session .codicon { +.debug-pane .session .codicon { line-height: 22px; margin-right: 2px; } -.monaco-workbench .debug-pane .debug-call-stack .monaco-action-bar .action-item > .action-label { +.monaco-workbench .debug-pane .monaco-action-bar .action-item > .action-label { width: 16px; height: 100%; line-height: 22px; @@ -259,11 +259,11 @@ font-family: var(--monaco-monospace-font); } -.debug-pane .monaco-inputbox > .wrapper { +.debug-pane .monaco-inputbox > .ibwrapper { height: 19px; } -.debug-pane .monaco-inputbox > .wrapper > .input { +.debug-pane .monaco-inputbox > .ibwrapper > .input { padding: 0px; color: initial; } @@ -319,7 +319,7 @@ } .debug-pane .debug-breakpoints .breakpoint > .file-path, -.debug-pane .debug-breakpoints .breakpoint.exception > .condition { +.debug-pane .debug-breakpoints .breakpoint > .condition { opacity: 0.7; margin-left: 0.9em; flex: 1; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 5030d532ab..cc2d3f518e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -71,6 +71,9 @@ text-overflow: ellipsis; white-space: nowrap; text-align: right; + /*Use direction so the source shows elipses on the left*/ + direction: rtl; + max-width: 400px; } .monaco-workbench .repl .repl-tree .output.expression > .value, diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 3cf01edb70..ff1f2a488c 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -82,6 +82,7 @@ export class RawDebugSession implements IDisposable { constructor( debugAdapter: IDebugAdapter, dbgr: IDebugger, + private readonly sessionId: string, private readonly telemetryService: ITelemetryService, public readonly customTelemetryService: ITelemetryService | undefined, private readonly extensionHostDebugService: IExtensionHostDebugService, @@ -154,6 +155,10 @@ export class RawDebugSession implements IDisposable { case 'invalidated': this._onDidInvalidated.fire(event as DebugProtocol.InvalidatedEvent); break; + case 'process': + break; + case 'module': + break; default: this._onDidCustomEvent.fire(event); break; @@ -579,7 +584,7 @@ export class RawDebugSession implements IDisposable { break; case 'runInTerminal': try { - const shellProcessId = await dbgr.runInTerminal(request.arguments as DebugProtocol.RunInTerminalRequestArguments); + const shellProcessId = await dbgr.runInTerminal(request.arguments as DebugProtocol.RunInTerminalRequestArguments, this.sessionId); const resp = response as DebugProtocol.RunInTerminalResponse; resp.body = {}; if (typeof shellProcessId === 'number') { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 2001f2fa40..e855390d5b 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/repl'; import { URI as uri } from 'vs/base/common/uri'; -import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/actions'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -14,11 +14,11 @@ import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { ITextModel } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; -import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; +import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -26,7 +26,7 @@ import { memoize } from 'vs/base/common/decorators'; import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IDebugService, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID, CONTEXT_MULTI_SESSION_REPL, CONTEXT_DEBUG_STATE, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { HistoryNavigator } from 'vs/base/common/history'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/browser/contextScopedHistoryWidget'; @@ -50,7 +50,7 @@ import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider, ReplGroupRenderer } from 'vs/workbench/contrib/debug/browser/replViewer'; import { localize } from 'vs/nls'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -60,6 +60,8 @@ import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/edit import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; import { ReplFilter, ReplFilterState, ReplFilterActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, MenuId, Action2, IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const $ = dom.$; @@ -73,17 +75,19 @@ function revealLastElement(tree: WorkbenchAsyncDataTree) { } const sessionsToIgnore = new Set(); +const identityProvider = { getId: (element: IReplElement) => element.getId() }; export class Repl extends ViewPane implements IHistoryNavigationWidget { declare readonly _serviceBrand: undefined; - private static readonly REFRESH_DELAY = 100; // delay in ms to refresh the repl for new elements to show + private static readonly REFRESH_DELAY = 50; // delay in ms to refresh the repl for new elements to show private static readonly URI = uri.parse(`${DEBUG_SCHEME}:replinput`); private history: HistoryNavigator; private tree!: WorkbenchAsyncDataTree; private replDelegate!: ReplDelegate; private container!: HTMLElement; + private treeContainer!: HTMLElement; private replInput!: CodeEditorWidget; private replInputContainer!: HTMLElement; private dimension!: dom.Dimension; @@ -98,6 +102,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private filter: ReplFilter; private filterState: ReplFilterState; private filterActionViewItem: ReplFilterActionViewItem | undefined; + private multiSessionRepl: IContextKey; + private menu: IMenu; constructor( options: IViewPaneOptions, @@ -112,17 +118,21 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, - @IClipboardService private readonly clipboardService: IClipboardService, @IEditorService private readonly editorService: IEditorService, @IKeybindingService keybindingService: IKeybindingService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService); + this._register(this.menu); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); this.filter = new ReplFilter(); this.filterState = new ReplFilterState(this); + this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); + this.multiSessionRepl.set(this.isMultiSessionView); codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); @@ -207,7 +217,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (!input || input.state === State.Inactive) { await this.selectSession(newSession); } - this.updateActions(); + this.multiSessionRepl.set(this.isMultiSessionView); })); this._register(this.themeService.onDidColorThemeChange(() => { this.refreshReplElements(false); @@ -224,10 +234,16 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInput.setModel(this.model); this.updateInputDecoration(); this.refreshReplElements(true); + this.layoutBody(this.dimension.height, this.dimension.width); } })); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { + if (e.affectsConfiguration('debug.console.wordWrap')) { + this.tree.dispose(); + this.treeContainer.innerText = ''; + dom.clearNode(this.treeContainer); + this.createReplTree(); + } else if (e.affectsConfiguration('debug.console.lineHeight') || e.affectsConfiguration('debug.console.fontSize') || e.affectsConfiguration('debug.console.fontFamily')) { this.onDidStyleChange(); } })); @@ -305,7 +321,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (this.styleElement) { const debugConsole = this.configurationService.getValue('debug').console; const fontSize = debugConsole.fontSize; - const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : debugConsole.fontFamily; + const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `${debugConsole.fontFamily}`; const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em'; const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor()); @@ -388,7 +404,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // Ignore inactive sessions which got cleared - so they are not shown any more sessionsToIgnore.add(session); await this.selectSession(); - this.updateActions(); + this.multiSessionRepl.set(this.isMultiSessionView); } } this.replInput.focus(); @@ -446,14 +462,22 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInput.layout({ width: width - 30, height: replInputHeight }); } + collapseAll(): void { + this.tree.collapseAll(); + } + + getReplInput(): CodeEditorWidget { + return this.replInput; + } + focus(): void { setTimeout(() => this.replInput.focus(), 0); } getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === SelectReplAction.ID) { + if (action.id === selectReplCommandId) { const session = (this.tree ? this.tree.getInput() : undefined) ?? this.debugService.getViewModel().focusedSession; - return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction, session); + return this.instantiationService.createInstance(SelectReplActionViewItem, action, session); } else if (action.id === FILTER_ACTION_ID) { const filterHistory = JSON.parse(this.storageService.get(FILTER_HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')) as string[]; this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, @@ -464,29 +488,11 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { return super.getActionViewItem(action); } - getActions(): IAction[] { - const result: IAction[] = []; - result.push(new Action(FILTER_ACTION_ID)); - if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) { - result.push(this.selectReplAction); - } - result.push(this.clearReplAction); - - result.forEach(a => this._register(a)); - - return result; + private get isMultiSessionView(): boolean { + return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1; } // --- Cached locals - @memoize - private get selectReplAction(): SelectReplAction { - return this.instantiationService.createInstance(SelectReplAction, SelectReplAction.ID, SelectReplAction.LABEL); - } - - @memoize - private get clearReplAction(): ClearReplAction { - return this.instantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL); - } @memoize private get refreshScheduler(): RunOnceScheduler { @@ -497,7 +503,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { } const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - await this.tree.updateChildren(); + await this.tree.updateChildren(undefined, true, false, { diffIdentityProvider: identityProvider }); const session = this.tree.getInput(); if (session) { @@ -532,25 +538,27 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { protected renderBody(parent: HTMLElement): void { super.renderBody(parent); - this.container = dom.append(parent, $('.repl')); - const treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); + this.treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); this.createReplInput(this.container); + this.createReplTree(); + } + private createReplTree(): void { this.replDelegate = new ReplDelegate(this.configurationService); const wordWrap = this.configurationService.getValue('debug').console.wordWrap; - treeContainer.classList.toggle('word-wrap', wordWrap); + this.treeContainer.classList.toggle('word-wrap', wordWrap); const linkDetector = this.instantiationService.createInstance(LinkDetector); this.tree = >this.instantiationService.createInstance( WorkbenchAsyncDataTree, 'DebugRepl', - treeContainer, + this.treeContainer, this.replDelegate, [ this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector), new ReplEvaluationInputsRenderer(), - new ReplGroupRenderer(), + this.instantiationService.createInstance(ReplGroupRenderer, linkDetector), new ReplEvaluationResultsRenderer(linkDetector), new ReplRawObjectsRenderer(linkDetector), ], @@ -559,9 +567,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { { filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), - identityProvider: { getId: (element: IReplElement) => element.getId() }, + identityProvider, mouseSupport: false, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e }, + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IReplElement) => e.toString(true) }, horizontalScrolling: !wordWrap, setRowLineHeight: false, supportDynamicHeights: wordWrap, @@ -589,7 +597,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInputContainer = dom.append(container, $('.repl-input-wrapper')); dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt))); - const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: this.replInputContainer, historyNavigator: this }); + const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: container, historyNavigator: this }); this.historyNavigationEnablement = historyNavigationEnablement; this._register(scopedContextKeyService); CONTEXT_IN_DEBUG_REPL.bindTo(scopedContextKeyService).set(true); @@ -620,44 +628,12 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private onContextMenu(e: ITreeContextMenuEvent): void { const actions: IAction[] = []; - actions.push(new Action('debug.replCopy', localize('copy', "Copy"), undefined, true, async () => { - const nativeSelection = window.getSelection(); - if (nativeSelection) { - await this.clipboardService.writeText(nativeSelection.toString()); - } - return Promise.resolve(); - })); - actions.push(new Action('workbench.debug.action.copyAll', localize('copyAll', "Copy All"), undefined, true, async () => { - await this.clipboardService.writeText(this.getVisibleContent()); - return Promise.resolve(); - })); - actions.push(new Action('debug.replPaste', localize('paste', "Paste"), undefined, this.debugService.state !== State.Inactive, async () => { - const clipboardText = await this.clipboardService.readText(); - if (clipboardText) { - this.replInput.setValue(this.replInput.getValue().concat(clipboardText)); - this.replInput.focus(); - const model = this.replInput.getModel(); - const lineNumber = model ? model.getLineCount() : 0; - const column = model?.getLineMaxColumn(lineNumber); - if (typeof lineNumber === 'number' && typeof column === 'number') { - this.replInput.setPosition({ lineNumber, column }); - } - } - })); - actions.push(new Separator()); - actions.push(new Action('debug.collapseRepl', localize('collapse', "Collapse All"), undefined, true, () => { - this.tree.collapseAll(); - this.replInput.focus(); - return Promise.resolve(); - })); - actions.push(new Separator()); - actions.push(this.clearReplAction); - + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, actions); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => e.element, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } @@ -816,48 +792,183 @@ class SelectReplActionViewItem extends FocusSessionActionViewItem { } } -class SelectReplAction extends Action { - - static readonly ID = 'workbench.action.debug.selectRepl'; - static readonly LABEL = localize('selectRepl', "Select Debug Console"); - - constructor(id: string, label: string, - @IDebugService private readonly debugService: IDebugService, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - async run(session: IDebugSession): Promise { - // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event - if (session && session.state !== State.Inactive && session !== this.debugService.getViewModel().focusedSession) { - await this.debugService.focusStackFrame(undefined, undefined, session, true); - } else { - const repl = getReplView(this.viewsService); - if (repl) { - await repl.selectSession(session); - } - } - } -} - -export class ClearReplAction extends Action { - static readonly ID = 'workbench.debug.panel.action.clearReplAction'; - static readonly LABEL = localize('clearRepl', "Clear Console"); - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, 'debug-action ' + ThemeIcon.asClassName(debugConsoleClearAll)); - } - - async run(): Promise { - const view = await this.viewsService.openView(REPL_VIEW_ID) as Repl; - await view.clearRepl(); - aria.status(localize('debugConsoleCleared', "Debug console was cleared")); - } -} - function getReplView(viewsService: IViewsService): Repl | undefined { return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined; } + +registerAction2(class extends Action2 { + constructor() { + super({ + id: FILTER_ACTION_ID, + title: localize('filter', "Filter"), + f1: true, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), + order: 10 + } + }); + } + + run(_accessor: ServicesAccessor) { + // noop this action is just a placeholder for the filter action view item + } +}); + +const selectReplCommandId = 'workbench.action.debug.selectRepl'; +registerAction2(class extends ViewAction { + constructor() { + super({ + id: selectReplCommandId, + viewId: REPL_VIEW_ID, + title: localize('selectRepl', "Select Debug Console"), + f1: true, + icon: debugConsoleClearAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), CONTEXT_MULTI_SESSION_REPL), + order: 20 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl, session: IDebugSession | undefined) { + const debugService = accessor.get(IDebugService); + // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event + if (session && session.state !== State.Inactive && session !== debugService.getViewModel().focusedSession) { + if (session.state !== State.Stopped) { + // Focus child session instead if it is stopped #112595 + const stopppedChildSession = debugService.getModel().getSessions().find(s => s.parentSession === session); + if (stopppedChildSession) { + session = stopppedChildSession; + } + } + await debugService.focusStackFrame(undefined, undefined, session, true); + } else { + await view.selectSession(session); + } + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.debug.panel.action.clearReplAction', + viewId: REPL_VIEW_ID, + title: localize('clearRepl', "Clear Console"), + f1: true, + icon: debugConsoleClearAll, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', REPL_VIEW_ID), + order: 30 + }, { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 20 + }] + }); + } + + runInView(_accessor: ServicesAccessor, view: Repl): void { + view.clearRepl(); + aria.status(localize('debugConsoleCleared', "Debug console was cleared")); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.collapseRepl', + title: localize('collapse', "Collapse All"), + viewId: REPL_VIEW_ID, + menu: { + id: MenuId.DebugConsoleContext, + group: 'z_commands', + order: 10 + } + }); + } + + runInView(_accessor: ServicesAccessor, view: Repl): void { + view.collapseAll(); + view.focus(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'debug.replPaste', + title: localize('paste', "Paste"), + viewId: REPL_VIEW_ID, + precondition: CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Inactive)), + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 30 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl): Promise { + const clipboardService = accessor.get(IClipboardService); + const clipboardText = await clipboardService.readText(); + if (clipboardText) { + const replInput = view.getReplInput(); + replInput.setValue(replInput.getValue().concat(clipboardText)); + view.focus(); + const model = replInput.getModel(); + const lineNumber = model ? model.getLineCount() : 0; + const column = model?.getLineMaxColumn(lineNumber); + if (typeof lineNumber === 'number' && typeof column === 'number') { + replInput.setPosition({ lineNumber, column }); + } + } + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.debug.action.copyAll', + title: localize('copyAll', "Copy All"), + viewId: REPL_VIEW_ID, + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 20 + } + }); + } + + async runInView(accessor: ServicesAccessor, view: Repl): Promise { + const clipboardService = accessor.get(IClipboardService); + await clipboardService.writeText(view.getVisibleContent()); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'debug.replCopy', + title: localize('copy', "Copy"), + menu: { + id: MenuId.DebugConsoleContext, + group: '2_cutcopypaste', + order: 10 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const clipboardService = accessor.get(IClipboardService); + const nativeSelection = window.getSelection(); + if (nativeSelection) { + await clipboardService.writeText(nativeSelection.toString()); + } + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/replFilter.ts b/src/vs/workbench/contrib/debug/browser/replFilter.ts index 54146795bc..085de8032b 100644 --- a/src/vs/workbench/contrib/debug/browser/replFilter.ts +++ b/src/vs/workbench/contrib/debug/browser/replFilter.ts @@ -61,7 +61,7 @@ export class ReplFilter implements ITreeFilter { let includeQueryPresent = false; let includeQueryMatched = false; - const text = element.toString(); + const text = element.toString(true); for (let { type, query } of this._parsedQueries) { if (type === 'exclude' && ReplFilter.matchQuery(query, text)) { diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index eea1f4e642..6e3f67cf76 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -34,7 +34,7 @@ interface IReplEvaluationInputTemplateData { } interface IReplGroupTemplateData { - label: HighlightedLabel; + label: HTMLElement; } interface IReplEvaluationResultTemplateData { @@ -87,22 +87,28 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer { static readonly ID = 'replGroup'; + constructor( + private readonly linkDetector: LinkDetector, + @IThemeService private readonly themeService: IThemeService + ) { } + get templateId(): string { return ReplGroupRenderer.ID; } - renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { - const input = dom.append(container, $('.expression')); - const label = new HighlightedLabel(input, false); + renderTemplate(container: HTMLElement): IReplGroupTemplateData { + const label = dom.append(container, $('.expression')); return { label }; } renderElement(element: ITreeNode, _index: number, templateData: IReplGroupTemplateData): void { const replGroup = element.element; - templateData.label.set(replGroup.name, createMatches(element.filterData)); + dom.clearNode(templateData.label); + const result = handleANSIOutput(replGroup.name, this.linkDetector, this.themeService, undefined); + templateData.label.appendChild(result); } - disposeTemplate(_templateData: IReplEvaluationInputTemplateData): void { + disposeTemplate(_templateData: IReplGroupTemplateData): void { // noop } } @@ -300,15 +306,15 @@ export class ReplDelegate extends CachedListVirtualDelegate { protected estimateHeight(element: IReplElement, ignoreValueLength = false): number { const config = this.configurationService.getValue('debug'); - const rowHeight = Math.ceil(1.4 * config.console.fontSize); + const rowHeight = Math.ceil(1.3 * config.console.fontSize); const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length); const hasValue = (e: any): e is { value: string } => typeof e.value === 'string'; // Calculate a rough overestimation for the height - // For every 30 characters increase the number of lines needed - if (hasValue(element)) { + // For every 70 characters increase the number of lines needed beyond the first + if (hasValue(element) && !(element instanceof Variable)) { let value = element.value; - let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30)); + let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 70)); return valueRows * rowHeight; } @@ -338,6 +344,10 @@ export class ReplDelegate extends CachedListVirtualDelegate { } hasDynamicHeight(element: IReplElement): boolean { + if (element instanceof Variable) { + // Variables should always be in one line #111843 + return false; + } // Empty elements should not have dynamic height since they will be invisible return element.toString().length > 0; } @@ -383,7 +393,8 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider 1 ? localize('occurred', ", occured {0} times", element.count) : ''); + return element.value + (element instanceof SimpleReplElement && element.count > 1 ? localize({ key: 'occurred', comment: ['Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] }, + ", occured {0} times", element.count) : ''); } if (element instanceof RawObjectReplElement) { return localize('replRawObjectAriaLabel', "Debug console variable {0}, value {1}", element.name, element.value); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index aede62ac5f..aaf6c72849 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -3,20 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { Variable, Scope, ErrorScope, StackFrame, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction } from 'vs/base/common/actions'; -import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -26,17 +23,19 @@ import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, IMenu, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { debugCollapseAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { coalesce } from 'vs/base/common/arrays'; const $ = dom.$; let forgetScopes = true; @@ -179,10 +178,6 @@ export class VariablesView extends ViewPane { })); } - getActions(): IAction[] { - return [new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(debugCollapseAll))]; - } - layoutBody(width: number, height: number): void { super.layoutBody(height, width); this.tree.layout(width, height); @@ -192,6 +187,10 @@ export class VariablesView extends ViewPane { this.tree.domFocus(); } + collapseAll(): void { + this.tree.collapseAll(); + } + private onMouseDblClick(e: ITreeMouseEvent): void { const session = this.debugService.getViewModel().focusedSession; if (session && e.element instanceof Variable && session.capabilities.supportsSetVariable) { @@ -345,7 +344,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { const variable = expression; return { initialValue: expression.value, - ariaLabel: nls.localize('variableValueAriaLabel', "Type new variable value"), + ariaLabel: localize('variableValueAriaLabel', "Type new variable value"), validationOptions: { validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null }, @@ -368,15 +367,15 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { class VariablesAccessibilityProvider implements IListAccessibilityProvider { getWidgetAriaLabel(): string { - return nls.localize('variablesAriaTreeLabel', "Debug Variables"); + return localize('variablesAriaTreeLabel', "Debug Variables"); } getAriaLabel(element: IExpression | IScope): string | null { if (element instanceof Scope) { - return nls.localize('variableScopeAriaLabel', "Scope {0}", element.name); + return localize('variableScopeAriaLabel', "Scope {0}", element.name); } if (element instanceof Variable) { - return nls.localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value); + return localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value); } return null; @@ -392,14 +391,40 @@ CommandsRegistry.registerCommand({ } }); -export const COPY_VALUE_ID = 'debug.copyValue'; +export const COPY_VALUE_ID = 'workbench.debug.viewlet.action.copyValue'; CommandsRegistry.registerCommand({ id: COPY_VALUE_ID, - handler: async (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - if (variableInternalContext) { - const action = instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variableInternalContext, 'variables'); - await action.run(); + handler: async (accessor: ServicesAccessor, arg: Variable | Expression | unknown, ctx?: (Variable | Expression)[]) => { + const debugService = accessor.get(IDebugService); + const clipboardService = accessor.get(IClipboardService); + let elementContext = ''; + let elements: (Variable | Expression)[]; + if (arg instanceof Variable || arg instanceof Expression) { + elementContext = 'watch'; + elements = ctx ? ctx : []; + } else { + elementContext = 'variables'; + elements = variableInternalContext ? [variableInternalContext] : []; + } + + const stackFrame = debugService.getViewModel().focusedStackFrame; + const session = debugService.getViewModel().focusedSession; + if (!stackFrame || !session || elements.length === 0) { + return; + } + + const evalContext = session.capabilities.supportsClipboardContext ? 'clipboard' : elementContext; + const toEvaluate = elements.map(element => element instanceof Variable ? (element.evaluateName || element.value) : element.name); + + try { + const evaluations = await Promise.all(toEvaluate.map(expr => session.evaluate(expr, stackFrame.frameId, evalContext))); + const result = coalesce(evaluations).map(evaluation => evaluation.body.result); + if (result.length) { + clipboardService.writeText(result.join('\n')); + } + } catch (e) { + const result = elements.map(element => element.value); + clipboardService.writeText(result.join('\n')); } } }); @@ -433,3 +458,23 @@ CommandsRegistry.registerCommand({ } }); +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'variables.collapse', + viewId: VARIABLES_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VARIABLES_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: VariablesView) { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 882a547bae..45adef8151 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -3,20 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, WATCH_VIEW_ID, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; -import { AddWatchExpressionAction, RemoveAllWatchExpressionsAction, CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -26,13 +23,17 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { FuzzyScore } from 'vs/base/common/filters'; import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { debugCollapseAll } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { watchExpressionsRemoveAll, watchExpressionsAdd } from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { registerAction2, MenuId, Action2, IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; +import { Codicon } from 'vs/base/common/codicons'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreViewUpdates = false; @@ -43,6 +44,9 @@ export class WatchExpressionsView extends ViewPane { private watchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh = false; private tree!: WorkbenchAsyncDataTree; + private watchExpressionsExist: IContextKey; + private watchItemType: IContextKey; + private menu: IMenu; constructor( options: IViewletViewOptions, @@ -56,13 +60,19 @@ export class WatchExpressionsView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugWatchContext, contextKeyService); + this._register(this.menu); this.watchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; this.tree.updateChildren(); }, 50); + this.watchExpressionsExist = CONTEXT_WATCH_EXPRESSIONS_EXIST.bindTo(contextKeyService); + this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); + this.watchItemType = CONTEXT_WATCH_ITEM_TYPE.bindTo(contextKeyService); } renderBody(container: HTMLElement): void { @@ -98,6 +108,7 @@ export class WatchExpressionsView extends ViewPane { this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); this._register(this.debugService.getModel().onDidChangeWatchExpressions(async we => { + this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); if (!this.isBodyVisible()) { this.needsRefresh = true; } else { @@ -141,7 +152,10 @@ export class WatchExpressionsView extends ViewPane { this.tree.updateOptions({ horizontalScrolling: false }); } - this.tree.rerender(e); + if (e.name) { + // Only rerender if the input is already done since otherwise the tree is not yet aware of the new element + this.tree.rerender(e); + } } else if (!e && horizontalScrolling !== undefined) { this.tree.updateOptions({ horizontalScrolling: horizontalScrolling }); horizontalScrolling = undefined; @@ -158,12 +172,8 @@ export class WatchExpressionsView extends ViewPane { this.tree.domFocus(); } - getActions(): IAction[] { - return [ - new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService), - new CollapseAction(() => this.tree, true, 'explorer-action ' + ThemeIcon.asClassName(debugCollapseAll)), - new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService) - ]; + collapseAll(): void { + this.tree.collapseAll(); } private onMouseDblClick(e: ITreeMouseEvent): void { @@ -184,42 +194,17 @@ export class WatchExpressionsView extends ViewPane { private onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; - const anchor = e.anchor; - if (!anchor) { - return; - } + const selection = this.tree.getSelection(); + + this.watchItemType.set(element instanceof Expression ? 'expression' : element instanceof Variable ? 'variable' : undefined); const actions: IAction[] = []; - - if (element instanceof Expression) { - const expression = element; - actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Action('debug.editWatchExpression', nls.localize('editWatchExpression', "Edit Expression"), undefined, true, () => { - this.debugService.getViewModel().setSelectedExpression(expression); - return Promise.resolve(); - })); - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, expression, 'watch')); - actions.push(new Separator()); - - actions.push(new Action('debug.removeWatchExpression', nls.localize('removeWatchExpression', "Remove Expression"), undefined, true, () => { - this.debugService.removeWatchExpressions(expression.getId()); - return Promise.resolve(); - })); - actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); - } else { - actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService)); - if (element instanceof Variable) { - const variable = element as Variable; - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'watch')); - actions.push(new Separator()); - } - actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService)); - } + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: element, shouldForwardArgs: true }, actions); this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, + getAnchor: () => e.anchor, getActions: () => actions, - getActionsContext: () => element, - onHide: () => dispose(actions) + getActionsContext: () => element && selection.includes(element) ? selection : element ? [element] : [], + onHide: () => dispose(actionsDisposable) }); } } @@ -287,8 +272,8 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { protected getInputBoxOptions(expression: IExpression): IInputBoxOptions { return { initialValue: expression.name ? expression.name : '', - ariaLabel: nls.localize('watchExpressionInputAriaLabel', "Type watch expression"), - placeholder: nls.localize('watchExpressionPlaceholder', "Expression to watch"), + ariaLabel: localize('watchExpressionInputAriaLabel', "Type watch expression"), + placeholder: localize('watchExpressionPlaceholder', "Expression to watch"), onFinish: (value: string, success: boolean) => { if (success && value) { this.debugService.renameWatchExpression(expression.getId(), value); @@ -306,16 +291,16 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { class WatchExpressionsAccessibilityProvider implements IListAccessibilityProvider { getWidgetAriaLabel(): string { - return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"); + return localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"); } getAriaLabel(element: IExpression): string { if (element instanceof Expression) { - return nls.localize('watchExpressionAriaLabel', "{0}, value {1}", (element).name, (element).value); + return localize('watchExpressionAriaLabel', "{0}, value {1}", (element).name, (element).value); } // Variable - return nls.localize('watchVariableAriaLabel', "{0}, value {1}", (element).name, (element).value); + return localize('watchVariableAriaLabel', "{0}, value {1}", (element).name, (element).value); } } @@ -359,3 +344,75 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { this.debugService.moveWatchExpression(draggedElement.getId(), position); } } + +registerAction2(class Collapse extends ViewAction { + constructor() { + super({ + id: 'watch.collapse', + viewId: WATCH_VIEW_ID, + title: localize('collapse', "Collapse All"), + f1: false, + icon: Codicon.collapseAll, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + menu: { + id: MenuId.ViewTitle, + order: 30, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + runInView(_accessor: ServicesAccessor, view: WatchExpressionsView) { + view.collapseAll(); + } +}); + +export const ADD_WATCH_ID = 'workbench.debug.viewlet.action.addWatchExpression'; // Use old and long id for backwards compatibility +export const ADD_WATCH_LABEL = localize('addWatchExpression', "Add Expression"); + +registerAction2(class AddWatchExpressionAction extends Action2 { + constructor() { + super({ + id: ADD_WATCH_ID, + title: ADD_WATCH_LABEL, + f1: false, + icon: watchExpressionsAdd, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.addWatchExpression(); + } +}); + +export const REMOVE_WATCH_EXPRESSIONS_COMMAND_ID = 'workbench.debug.viewlet.action.removeAllWatchExpressions'; +export const REMOVE_WATCH_EXPRESSIONS_LABEL = localize('removeAllWatchExpressions', "Remove All Expressions"); +registerAction2(class RemoveAllWatchExpressionsAction extends Action2 { + constructor() { + super({ + id: REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, // Use old and long id for backwards compatibility + title: REMOVE_WATCH_EXPRESSIONS_LABEL, + f1: false, + icon: watchExpressionsRemoveAll, + precondition: CONTEXT_WATCH_EXPRESSIONS_EXIST, + menu: { + id: MenuId.ViewTitle, + order: 20, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', WATCH_VIEW_ID) + } + }); + } + + run(accessor: ServicesAccessor): void { + const debugService = accessor.get(IDebugService); + debugService.removeWatchExpressions(); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index 27647659d1..d3c0df420a 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -10,10 +10,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewDescriptorService, IViewsRegistry, Extensions, ViewContentGroups } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -25,6 +24,7 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; const debugStartLanguageKey = 'debugStartLanguage'; const CONTEXT_DEBUG_START_LANGUAGE = new RawContextKey(debugStartLanguageKey, undefined); @@ -96,7 +96,7 @@ export class WelcomeView extends ViewPane { })); setContextKey(); - const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); + const debugKeybinding = this.keybindingService.lookupKeybinding(DEBUG_START_COMMAND_ID); debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; } @@ -116,14 +116,14 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), + "[Run and Debug{0}](command:{1})", debugKeybindingLabel, DEBUG_START_COMMAND_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), + "[Show](command:{0}) all automatic debug configurations.", SELECT_AND_START_ID), when: CONTEXT_DEBUGGERS_AVAILABLE, group: ViewContentGroups.Debug, order: 10 @@ -131,7 +131,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, - "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), + "To customize Run and Debug [create a launch.json file](command:{0}).", DEBUG_CONFIGURE_COMMAND_ID), when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')), group: ViewContentGroups.Debug }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 27cf0b542c..5e99ab27ca 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -25,6 +25,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { IAction } from 'vs/base/common/actions'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -47,10 +48,14 @@ export const CONTEXT_BREAKPOINT_WIDGET_VISIBLE = new RawContextKey('bre export const CONTEXT_IN_BREAKPOINT_WIDGET = new RawContextKey('inBreakpointWidget', false); export const CONTEXT_BREAKPOINTS_FOCUSED = new RawContextKey('breakpointsFocused', true); export const CONTEXT_WATCH_EXPRESSIONS_FOCUSED = new RawContextKey('watchExpressionsFocused', true); +export const CONTEXT_WATCH_EXPRESSIONS_EXIST = new RawContextKey('watchExpressionsExist', false); export const CONTEXT_VARIABLES_FOCUSED = new RawContextKey('variablesFocused', true); export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey('expressionSelected', false); -export const CONTEXT_BREAKPOINT_SELECTED = new RawContextKey('breakpointSelected', false); +export const CONTEXT_BREAKPOINT_INPUT_FOCUSED = new RawContextKey('breakpointInputFocused', false); export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey('callStackItemType', undefined); +export const CONTEXT_WATCH_ITEM_TYPE = new RawContextKey('watchItemType', undefined); +export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined); +export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false); export const CONTEXT_LOADED_SCRIPTS_ITEM_TYPE = new RawContextKey('loadedScriptsItemType', undefined); export const CONTEXT_FOCUSED_SESSION_IS_ATTACH = new RawContextKey('focusedSessionIsAttach', false); @@ -65,6 +70,8 @@ export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugS export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false); export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey('variableEvaluateNamePresent', false); export const CONTEXT_EXCEPTION_WIDGET_VISIBLE = new RawContextKey('exceptionWidgetVisible', false); +export const CONTEXT_MULTI_SESSION_REPL = new RawContextKey('multiSessionRepl', false); +export const CONTEXT_MULTI_SESSION_DEBUG = new RawContextKey('multiSessionDebug', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint'; @@ -100,7 +107,7 @@ export interface ITreeElement { } export interface IReplElement extends ITreeElement { - toString(): string; + toString(includeSource?: boolean): string; readonly sourceData?: IReplElementSource; } @@ -125,7 +132,7 @@ export interface IExpression extends IExpressionContainer { export interface IDebugger { createDebugAdapter(session: IDebugSession): Promise; - runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise; + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise; getCustomTelemetryService(): Promise; } @@ -183,6 +190,7 @@ export interface IDebugSession extends ITreeElement { readonly subId: string | undefined; readonly compact: boolean; readonly compoundRoot: DebugCompoundRoot | undefined; + readonly name: string; setSubId(subId: string | undefined): void; @@ -437,9 +445,7 @@ export interface IViewModel extends ITreeElement { readonly focusedStackFrame: IStackFrame | undefined; getSelectedExpression(): IExpression | undefined; - getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined; setSelectedExpression(expression: IExpression | undefined): void; - setSelectedBreakpoint(functionBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void; updateViews(): void; isMultiSessionView(): boolean; @@ -498,6 +504,7 @@ export interface IDebugConfiguration { lineHeight: number; wordWrap: boolean; closeOnEnd: boolean; + collapseIdenticalLines: boolean; historySuggestions: boolean; }; focusWindowOnBreak: boolean; @@ -655,7 +662,7 @@ export interface IDebugAdapterTrackerFactory { } export interface ITerminalLauncher { - runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise; + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise; } export interface IConfigurationManager { @@ -709,7 +716,7 @@ export interface IAdapterManager { unregisterDebugAdapterDescriptorFactory(debugAdapterDescriptorFactory: IDebugAdapterDescriptorFactory): void; substituteVariables(debugType: string, folder: IWorkspaceFolder | undefined, config: IConfig): Promise; - runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments): Promise; + runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise; } export interface ILaunch { @@ -844,10 +851,10 @@ export interface IDebugService { addFunctionBreakpoint(name?: string, id?: string): void; /** - * Renames an already existing function breakpoint. + * Updates an already existing function breakpoint. * Notifies debug adapter of breakpoint changes. */ - renameFunctionBreakpoint(id: string, newFunctionName: string): Promise; + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise; /** * Removes all function breakpoints. If id is passed only removes the function breakpoint with the passed id. @@ -868,6 +875,8 @@ export interface IDebugService { setExceptionBreakpointCondition(breakpoint: IExceptionBreakpoint, condition: string | undefined): Promise; + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void; + /** * Sends all breakpoints to the passed session. * If session is not passed, sends all breakpoints to each session. @@ -946,6 +955,7 @@ export interface IDebugEditorContribution extends editorCommon.IEditorContributi export interface IBreakpointEditorContribution extends editorCommon.IEditorContribution { showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void; closeBreakpointWidget(): void; + getContextMenuActionsAtPosition(lineNumber: number, model: EditorIModel): IAction[]; } // temporary debug helper service diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 2e32e7e397..74cf468add 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -423,7 +423,9 @@ export class Thread implements IThread { } getTopStackFrame(): IStackFrame | undefined { - return this.getCallStack().find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + const callStack = this.getCallStack(); + const firstAvailableStackFrame = callStack.find(sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + return firstAvailableStackFrame || (callStack.length > 0 ? callStack[0] : undefined); } get stateLabel(): string { @@ -452,6 +454,9 @@ export class Thread implements IThread { this.callStack.splice(start, this.callStack.length - start); } this.callStack = this.callStack.concat(callStack || []); + if (typeof this.stoppedDetails?.totalFrames === 'number' && this.stoppedDetails.totalFrames === this.callStack.length) { + this.reachedEndOfCallStack = true; + } } } @@ -946,6 +951,11 @@ export class DebugModel implements IDebugModel { return true; }); + let i = 1; + while (this.sessions.some(s => s.getLabel() === session.getLabel())) { + session.setName(`${session.configuration.name} ${++i}`); + } + let index = -1; if (session.parentSession) { // Make sure that child sessions are placed after the parent session @@ -1236,10 +1246,18 @@ export class DebugModel implements IDebugModel { return newFunctionBreakpoint; } - renameFunctionBreakpoint(id: string, name: string): void { + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): void { const functionBreakpoint = this.functionBreakpoints.find(fbp => fbp.getId() === id); if (functionBreakpoint) { - functionBreakpoint.name = name; + if (typeof update.name === 'string') { + functionBreakpoint.name = update.name; + } + if (typeof update.condition === 'string') { + functionBreakpoint.condition = update.condition; + } + if (typeof update.hitCondition === 'string') { + functionBreakpoint.hitCondition = update.hitCondition; + } this._onDidChangeBreakpoints.fire({ changed: [functionBreakpoint], sessionOnly: false }); } } diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 8074925152..19297f1e1a 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -740,7 +740,7 @@ declare module DebugProtocol { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; /** The name of the Variable's child to obtain data breakpoint information for. - If variableReference isn’t provided, this can be an expression. + If variablesReference isn’t provided, this can be an expression. */ name: string; } @@ -1012,7 +1012,7 @@ declare module DebugProtocol { /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state of a given thread. - A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. + A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. */ export interface StackTraceRequest extends Request { // command: 'stackTrace'; @@ -1591,7 +1591,7 @@ declare module DebugProtocol { supportsExceptionInfoRequest?: boolean; /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ supportTerminateDebuggee?: boolean; - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and the 'totalFrames' result of the 'StackTrace' request are supported. */ + /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and an optional 'totalFrames' result of the 'StackTrace' request are supported. */ supportsDelayedStackTraceLoading?: boolean; /** The debug adapter supports the 'loadedSources' request. */ supportsLoadedSourcesRequest?: boolean; @@ -1871,7 +1871,7 @@ declare module DebugProtocol { 'mostDerivedClass': Indicates that the object is the most derived class. 'virtual': Indicates that the object is virtual, that means it is a synthetic object introducedby the adapter for rendering purposes, e.g. an index range for large arrays. - 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. + 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is registered for the object. The 'hasDataBreakpoint' attribute should generally be used instead. etc. */ kind?: 'property' | 'method' | 'class' | 'data' | 'event' | 'baseClass' | 'innerClass' | 'interface' | 'mostDerivedClass' | 'virtual' | 'dataBreakpoint' | string; @@ -1884,9 +1884,10 @@ declare module DebugProtocol { 'hasObjectId': Indicates that the object can have an Object ID created for it. 'canHaveObjectId': Indicates that the object has an Object ID associated with it. 'hasSideEffects': Indicates that the evaluation had side effects. + 'hasDataBreakpoint': Indicates that the object has its value tracked by a data breakpoint. etc. */ - attributes?: ('static' | 'constant' | 'readOnly' | 'rawString' | 'hasObjectId' | 'canHaveObjectId' | 'hasSideEffects' | string)[]; + attributes?: ('static' | 'constant' | 'readOnly' | 'rawString' | 'hasObjectId' | 'canHaveObjectId' | 'hasSideEffects' | 'hasDataBreakpoint' | string)[]; /** Visibility of variable. Before introducing additional values, try to use the listed values. Values: 'public', 'private', 'protected', 'internal', 'final', etc. */ diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index 3be8546238..87a01e93e1 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -15,6 +15,7 @@ const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; const DEBUG_DATA_BREAKPOINTS_KEY = 'debug.databreakpoint'; const DEBUG_EXCEPTION_BREAKPOINTS_KEY = 'debug.exceptionbreakpoint'; const DEBUG_WATCH_EXPRESSIONS_KEY = 'debug.watchexpressions'; +const DEBUG_UX_STATE_KEY = 'debug.uxstate'; export class DebugStorage { constructor( @@ -23,6 +24,14 @@ export class DebugStorage { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { } + loadDebugUxState(): 'simple' | 'default' { + return this.storageService.get(DEBUG_UX_STATE_KEY, StorageScope.WORKSPACE, 'simple') as 'simple' | 'default'; + } + + storeDebugUxState(value: 'simple' | 'default'): void { + this.storageService.store(DEBUG_UX_STATE_KEY, value, StorageScope.WORKSPACE, StorageTarget.USER); + } + loadBreakpoints(): Breakpoint[] { let result: Breakpoint[] | undefined; try { diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 8a00d12862..ef152084b0 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, IExceptionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -16,14 +16,11 @@ export class ViewModel implements IViewModel { private _focusedSession: IDebugSession | undefined; private _focusedThread: IThread | undefined; private selectedExpression: IExpression | undefined; - private selectedBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined; private readonly _onDidFocusSession = new Emitter(); private readonly _onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame | undefined, explicit: boolean }>(); private readonly _onDidSelectExpression = new Emitter(); private readonly _onWillUpdateViews = new Emitter(); - private multiSessionView: boolean; private expressionSelectedContextKey!: IContextKey; - private breakpointSelectedContextKey!: IContextKey; private loadedScriptsSupportedContextKey!: IContextKey; private stepBackSupportedContextKey!: IContextKey; private focusedSessionIsAttach!: IContextKey; @@ -31,12 +28,11 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private multiSessionDebug!: IContextKey; constructor(private contextKeyService: IContextKeyService) { - this.multiSessionView = false; contextKeyService.bufferChangeEvents(() => { this.expressionSelectedContextKey = CONTEXT_EXPRESSION_SELECTED.bindTo(contextKeyService); - this.breakpointSelectedContextKey = CONTEXT_BREAKPOINT_SELECTED.bindTo(contextKeyService); this.loadedScriptsSupportedContextKey = CONTEXT_LOADED_SCRIPTS_SUPPORTED.bindTo(contextKeyService); this.stepBackSupportedContextKey = CONTEXT_STEP_BACK_SUPPORTED.bindTo(contextKeyService); this.focusedSessionIsAttach = CONTEXT_FOCUSED_SESSION_IS_ATTACH.bindTo(contextKeyService); @@ -44,6 +40,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); }); } @@ -112,10 +109,6 @@ export class ViewModel implements IViewModel { return this._onDidSelectExpression.event; } - getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined { - return this.selectedBreakpoint; - } - updateViews(): void { this._onWillUpdateViews.fire(); } @@ -124,16 +117,11 @@ export class ViewModel implements IViewModel { return this._onWillUpdateViews.event; } - setSelectedBreakpoint(breakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void { - this.selectedBreakpoint = breakpoint; - this.breakpointSelectedContextKey.set(!!breakpoint); - } - isMultiSessionView(): boolean { - return this.multiSessionView; + return !!this.multiSessionDebug.get(); } setMultiSessionView(isMultiSessionView: boolean): void { - this.multiSessionView = isMultiSessionView; + this.multiSessionDebug.set(isMultiSessionView); } } diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 6efb4d78dc..2a2632072b 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -113,8 +113,8 @@ export class Debugger implements IDebugger { }); } - runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise { - return this.adapterManager.runInTerminal(this.type, args); + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise { + return this.adapterManager.runInTerminal(this.type, args, sessionId); } get label(): string { diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index d09e2afb6b..2ae226f4f4 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -5,13 +5,14 @@ import * as nls from 'vs/nls'; import severity from 'vs/base/common/severity'; -import { IReplElement, IStackFrame, IExpression, IReplElementSource, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IReplElement, IStackFrame, IExpression, IReplElementSource, IDebugSession, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { ExpressionContainer } from 'vs/workbench/contrib/debug/common/debugModel'; import { isString, isUndefinedOrNull, isObject } from 'vs/base/common/types'; import { basenameOrAuthority } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Emitter, Event } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const MAX_REPL_LENGTH = 10000; let topReplElementCounter = 0; @@ -29,9 +30,13 @@ export class SimpleReplElement implements IReplElement { public sourceData?: IReplElementSource, ) { } - toString(): string { - const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}` : ''; - return this.value + sourceStr; + toString(includeSource = false): string { + let valueRespectCount = this.value; + for (let i = 1; i < this.count; i++) { + valueRespectCount += (valueRespectCount.endsWith('\n') ? '' : '\n') + this.value; + } + const sourceStr = (this.sourceData && includeSource) ? ` ${this.sourceData.source.name}` : ''; + return valueRespectCount + sourceStr; } getId(): string { @@ -160,8 +165,8 @@ export class ReplGroup implements IReplElement { return this.id; } - toString(): string { - const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}` : ''; + toString(includeSource = false): string { + const sourceStr = (includeSource && this.sourceData) ? ` ${this.sourceData.source.name}` : ''; return this.name + sourceStr; } @@ -197,6 +202,8 @@ export class ReplModel { private readonly _onDidChangeElements = new Emitter(); readonly onDidChangeElements = this._onDidChangeElements.event; + constructor(private readonly configurationService: IConfigurationService) { } + getReplElements(): IReplElement[] { return this.replElements; } @@ -220,7 +227,8 @@ export class ReplModel { if (typeof data === 'string') { const previousElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined; if (previousElement instanceof SimpleReplElement && previousElement.severity === sev) { - if (previousElement.value === data) { + const config = this.configurationService.getValue('debug'); + if (previousElement.value === data && config.console.collapseIdenticalLines) { previousElement.count++; // No need to fire an event, just the count updates and badge will adjust automatically return; diff --git a/src/vs/workbench/contrib/debug/node/debugHelperService.ts b/src/vs/workbench/contrib/debug/node/debugHelperService.ts index 591f8f0d0a..4e73fdd436 100644 --- a/src/vs/workbench/contrib/debug/node/debugHelperService.ts +++ b/src/vs/workbench/contrib/debug/node/debugHelperService.ts @@ -5,7 +5,7 @@ import { IDebugHelperService } from 'vs/workbench/contrib/debug/common/debug'; import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; -import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { FileAccess } from 'vs/base/common/network'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,8 +31,8 @@ export class NodeDebugHelperService implements IDebugHelperService { args: args, env: { ELECTRON_RUN_AS_NODE: 1, - PIPE_LOGGING: 'true', - AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp' + VSCODE_PIPE_LOGGING: 'true', + VSCODE_AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp' } } ); diff --git a/src/vs/workbench/contrib/debug/node/telemetryApp.ts b/src/vs/workbench/contrib/debug/node/telemetryApp.ts index ca8004b0f0..63aadea23a 100644 --- a/src/vs/workbench/contrib/debug/node/telemetryApp.ts +++ b/src/vs/workbench/contrib/debug/node/telemetryApp.ts @@ -5,7 +5,7 @@ import { Server } from 'vs/base/parts/ipc/node/ipc.cp'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; +import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; const appender = new AppInsightsAppender(process.argv[2], JSON.parse(process.argv[3]), process.argv[4]); process.once('exit', () => appender.flush()); diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index b87c0830c7..9d1dd1965d 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -9,7 +9,7 @@ import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExtern import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; -import { extractDriveLetter } from 'vs/base/common/labels'; +import { getDriveLetter } from 'vs/base/common/extpath'; let externalTerminalService: IExternalTerminalService | undefined = undefined; @@ -112,7 +112,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? }; if (cwd) { - const driveLetter = extractDriveLetter(cwd); + const driveLetter = getDriveLetter(cwd); if (driveLetter) { command += `${driveLetter}:; `; } @@ -145,7 +145,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? }; if (cwd) { - const driveLetter = extractDriveLetter(cwd); + const driveLetter = getDriveLetter(cwd); if (driveLetter) { command += `${driveLetter}: && `; } diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 151aee1038..3334af10e1 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -153,8 +153,8 @@ suite('Debug - Breakpoints', () => { test('function breakpoints', () => { model.addFunctionBreakpoint('foo', '1'); model.addFunctionBreakpoint('bar', '2'); - model.renameFunctionBreakpoint('1', 'fooUpdated'); - model.renameFunctionBreakpoint('2', 'barUpdated'); + model.updateFunctionBreakpoint('1', { name: 'fooUpdated' }); + model.updateFunctionBreakpoint('2', { name: 'barUpdated' }); const functionBps = model.getFunctionBreakpoints(); assert.equal(functionBps[0].name, 'fooUpdated'); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index f83310f5d6..04d1767cdc 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -19,6 +19,15 @@ import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug import { generateUuid } from 'vs/base/common/uuid'; import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; + +const mockWorkspaceContextService = { + getWorkspace: () => { + return { + folders: [] + }; + } +} as any; export function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, { @@ -29,7 +38,7 @@ export function createMockSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { @@ -91,7 +100,7 @@ suite.skip('Debug - CallStack', () => { assert.equal(model.getSessions(true).length, 1); }); - test('threads multiple wtih allThreadsStopped', () => { + test('threads multiple wtih allThreadsStopped', async () => { const threadId1 = 1; const threadName1 = 'firstThread'; const threadId2 = 2; @@ -145,19 +154,16 @@ suite.skip('Debug - CallStack', () => { // after calling getCallStack, the callstack becomes available // and results in a request for the callstack in the debug adapter - thread1.fetchCallStack().then(() => { - assert.notEqual(thread1.getCallStack().length, 0); - }); + await thread1.fetchCallStack(); + assert.notEqual(thread1.getCallStack().length, 0); - thread2.fetchCallStack().then(() => { - assert.notEqual(thread2.getCallStack().length, 0); - }); + await thread2.fetchCallStack(); + assert.notEqual(thread2.getCallStack().length, 0); // calling multiple times getCallStack doesn't result in multiple calls // to the debug adapter - thread1.fetchCallStack().then(() => { - return thread2.fetchCallStack(); - }); + await thread1.fetchCallStack(); + await thread2.fetchCallStack(); // clearing the callstack results in the callstack not being available thread1.clearCallStack(); @@ -174,7 +180,7 @@ suite.skip('Debug - CallStack', () => { assert.equal(session.getAllThreads().length, 0); }); - test('threads mutltiple without allThreadsStopped', () => { + test('threads mutltiple without allThreadsStopped', async () => { const sessionStub = sinon.spy(rawSession, 'stackTrace'); const stoppedThreadId = 1; @@ -230,19 +236,17 @@ suite.skip('Debug - CallStack', () => { // after calling getCallStack, the callstack becomes available // and results in a request for the callstack in the debug adapter - stoppedThread.fetchCallStack().then(() => { - assert.notEqual(stoppedThread.getCallStack().length, 0); - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(sessionStub.callCount, 1); - }); + await stoppedThread.fetchCallStack(); + assert.notEqual(stoppedThread.getCallStack().length, 0); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); // calling getCallStack on the running thread returns empty array // and does not return in a request for the callstack in the debug // adapter - runningThread.fetchCallStack().then(() => { - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(sessionStub.callCount, 1); - }); + await runningThread.fetchCallStack(); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); // clearing the callstack results in the callstack not being available stoppedThread.clearCallStack(); @@ -374,7 +378,7 @@ suite.skip('Debug - CallStack', () => { get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); const runningSession = createMockSession(model); model.addSession(runningSession); diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts similarity index 100% rename from src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts rename to src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index a845aeb039..2aa223fd9d 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -64,7 +64,7 @@ suite('Debug - Link Detector', () => { test('singleLineLink', () => { const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; + const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; const output = linkDetector.linkify(input); assert.equal(1, output.children.length); @@ -87,7 +87,7 @@ suite('Debug - Link Detector', () => { test('relativeLinkWithWorkspace', () => { const input = '\./foo/bar.js'; - const expectedOutput = /^\.\/foo\/bar\.js<\/a><\/span>$/; + const expectedOutput = /^\.\/foo\/bar\.js<\/a><\/span>$/; const output = linkDetector.linkify(input, false, new WorkspaceFolder({ uri: URI.file('/path/to/workspace'), name: 'ws', index: 0 })); assert.equal('SPAN', output.tagName); @@ -96,7 +96,7 @@ suite('Debug - Link Detector', () => { test('singleLineLinkAndText', function () { const input = isWindows ? 'The link: C:/foo/bar.js:12:34' : 'The link: /Users/foo/bar.js:12:34'; - const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; + const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; const output = linkDetector.linkify(input); assert.equal(1, output.children.length); @@ -110,7 +110,7 @@ suite('Debug - Link Detector', () => { test('singleLineMultipleLinks', () => { const input = isWindows ? 'Here is a link C:/foo/bar.js:12:34 and here is another D:/boo/far.js:56:78' : 'Here is a link /Users/foo/bar.js:12:34 and here is another /Users/boo/far.js:56:78'; - const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; + const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; const output = linkDetector.linkify(input); assert.equal(2, output.children.length); @@ -152,7 +152,7 @@ suite('Debug - Link Detector', () => { test('multilineWithLinks', () => { const input = isWindows ? 'I have a link for you\nHere it is: C:/foo/bar.js:12:34\nCool, huh?' : 'I have a link for you\nHere it is: /Users/foo/bar.js:12:34\nCool, huh?'; - const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; + const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; const output = linkDetector.linkify(input, true); assert.equal(3, output.children.length); diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 1058c1525a..591234fa3e 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -24,29 +24,29 @@ export const mockUriIdentityService = new UriIdentityService(fileService); export class MockDebugService implements IDebugService { - public _serviceBrand: undefined; + _serviceBrand: undefined; - public get state(): State { + get state(): State { throw new Error('not implemented'); } - public get onWillNewSession(): Event { + get onWillNewSession(): Event { throw new Error('not implemented'); } - public get onDidNewSession(): Event { + get onDidNewSession(): Event { throw new Error('not implemented'); } - public get onDidEndSession(): Event { + get onDidEndSession(): Event { throw new Error('not implemented'); } - public get onDidChangeState(): Event { + get onDidChangeState(): Event { throw new Error('not implemented'); } - public getConfigurationManager(): IConfigurationManager { + getConfigurationManager(): IConfigurationManager { throw new Error('not implemented'); } @@ -58,7 +58,7 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public focusStackFrame(focusedStackFrame: IStackFrame): Promise { + focusStackFrame(focusedStackFrame: IStackFrame): Promise { throw new Error('not implemented'); } @@ -66,23 +66,23 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - public addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { + addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { throw new Error('not implemented'); } - public updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { + updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { throw new Error('not implemented'); } - public enableOrDisableBreakpoints(enabled: boolean): Promise { + enableOrDisableBreakpoints(enabled: boolean): Promise { throw new Error('not implemented'); } - public setBreakpointsActivated(): Promise { + setBreakpointsActivated(): Promise { throw new Error('not implemented'); } - public removeBreakpoints(): Promise { + removeBreakpoints(): Promise { throw new Error('not implemented'); } @@ -90,15 +90,19 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public addFunctionBreakpoint(): void { } + setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + throw new Error('Method not implemented.'); + } - public moveWatchExpression(id: string, position: number): void { } + addFunctionBreakpoint(): void { } - public renameFunctionBreakpoint(id: string, newFunctionName: string): Promise { + moveWatchExpression(id: string, position: number): void { } + + updateFunctionBreakpoint(id: string, update: { name?: string, hitCondition?: string, condition?: string }): Promise { throw new Error('not implemented'); } - public removeFunctionBreakpoints(id?: string): Promise { + removeFunctionBreakpoints(id?: string): Promise { throw new Error('not implemented'); } @@ -109,47 +113,47 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - public addReplExpression(name: string): Promise { + addReplExpression(name: string): Promise { throw new Error('not implemented'); } - public removeReplExpressions(): void { } + removeReplExpressions(): void { } - public addWatchExpression(name?: string): Promise { + addWatchExpression(name?: string): Promise { throw new Error('not implemented'); } - public renameWatchExpression(id: string, newName: string): Promise { + renameWatchExpression(id: string, newName: string): Promise { throw new Error('not implemented'); } - public removeWatchExpressions(id?: string): void { } + removeWatchExpressions(id?: string): void { } - public startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { + startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { return Promise.resolve(true); } - public restartSession(): Promise { + restartSession(): Promise { throw new Error('not implemented'); } - public stopSession(): Promise { + stopSession(): Promise { throw new Error('not implemented'); } - public getModel(): IDebugModel { + getModel(): IDebugModel { throw new Error('not implemented'); } - public getViewModel(): IViewModel { + getViewModel(): IViewModel { throw new Error('not implemented'); } - public logToRepl(session: IDebugSession, value: string): void { } + logToRepl(session: IDebugSession, value: string): void { } - public sourceIsNotAvailable(uri: uri): void { } + sourceIsNotAvailable(uri: uri): void { } - public tryToAutoFocusStackFrame(thread: IThread): Promise { + tryToAutoFocusStackFrame(thread: IThread): Promise { throw new Error('not implemented'); } } @@ -227,6 +231,10 @@ export class MockSession implements IDebugSession { return 'mockname'; } + get name(): string { + return 'mockname'; + } + setName(name: string): void { throw new Error('not implemented'); } @@ -387,14 +395,14 @@ export class MockRawSession { disconnected = false; sessionLengthInSeconds: number = 0; - public readyForBreakpoints = true; - public emittedStopped = true; + readyForBreakpoints = true; + emittedStopped = true; - public getLengthInSeconds(): number { + getLengthInSeconds(): number { return 100; } - public stackTrace(args: DebugProtocol.StackTraceArguments): Promise { + stackTrace(args: DebugProtocol.StackTraceArguments): Promise { return Promise.resolve({ seq: 1, type: 'response', @@ -412,19 +420,19 @@ export class MockRawSession { }); } - public exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { + exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { throw new Error('not implemented'); } - public launchOrAttach(args: IConfig): Promise { + launchOrAttach(args: IConfig): Promise { throw new Error('not implemented'); } - public scopes(args: DebugProtocol.ScopesArguments): Promise { + scopes(args: DebugProtocol.ScopesArguments): Promise { throw new Error('not implemented'); } - public variables(args: DebugProtocol.VariablesArguments): Promise { + variables(args: DebugProtocol.VariablesArguments): Promise { throw new Error('not implemented'); } @@ -432,87 +440,87 @@ export class MockRawSession { return Promise.resolve(null!); } - public custom(request: string, args: any): Promise { + custom(request: string, args: any): Promise { throw new Error('not implemented'); } - public terminate(restart = false): Promise { + terminate(restart = false): Promise { throw new Error('not implemented'); } - public disconnect(restart?: boolean): Promise { + disconnect(restart?: boolean): Promise { throw new Error('not implemented'); } - public threads(): Promise { + threads(): Promise { throw new Error('not implemented'); } - public stepIn(args: DebugProtocol.StepInArguments): Promise { + stepIn(args: DebugProtocol.StepInArguments): Promise { throw new Error('not implemented'); } - public stepOut(args: DebugProtocol.StepOutArguments): Promise { + stepOut(args: DebugProtocol.StepOutArguments): Promise { throw new Error('not implemented'); } - public stepBack(args: DebugProtocol.StepBackArguments): Promise { + stepBack(args: DebugProtocol.StepBackArguments): Promise { throw new Error('not implemented'); } - public continue(args: DebugProtocol.ContinueArguments): Promise { + continue(args: DebugProtocol.ContinueArguments): Promise { throw new Error('not implemented'); } - public reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { + reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { throw new Error('not implemented'); } - public pause(args: DebugProtocol.PauseArguments): Promise { + pause(args: DebugProtocol.PauseArguments): Promise { throw new Error('not implemented'); } - public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { + terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { throw new Error('not implemented'); } - public setVariable(args: DebugProtocol.SetVariableArguments): Promise { + setVariable(args: DebugProtocol.SetVariableArguments): Promise { throw new Error('not implemented'); } - public restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { + restartFrame(args: DebugProtocol.RestartFrameArguments): Promise { throw new Error('not implemented'); } - public completions(args: DebugProtocol.CompletionsArguments): Promise { + completions(args: DebugProtocol.CompletionsArguments): Promise { throw new Error('not implemented'); } - public next(args: DebugProtocol.NextArguments): Promise { + next(args: DebugProtocol.NextArguments): Promise { throw new Error('not implemented'); } - public source(args: DebugProtocol.SourceArguments): Promise { + source(args: DebugProtocol.SourceArguments): Promise { throw new Error('not implemented'); } - public loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { + loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { throw new Error('not implemented'); } - public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { + setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { throw new Error('not implemented'); } - public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { + setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { throw new Error('not implemented'); } - public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { + setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { throw new Error('not implemented'); } - public readonly onDidStop: Event = null!; + readonly onDidStop: Event = null!; } export class MockDebugAdapter extends AbstractDebugAdapter { diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index 104e4887ab..5812d4d13c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -14,10 +14,12 @@ import { timeout } from 'vs/base/common/async'; import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter'; import { TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; suite('Debug - REPL', () => { let model: DebugModel; let rawSession: MockRawSession; + const configurationService = new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }); setup(() => { model = createMockDebugModel(); @@ -26,7 +28,7 @@ suite('Debug - REPL', () => { test('repl output', () => { const session = createMockSession(model); - const repl = new ReplModel(); + const repl = new ReplModel(configurationService); repl.appendToRepl(session, 'first line\n', severity.Error); repl.appendToRepl(session, 'second line ', severity.Error); repl.appendToRepl(session, 'third line ', severity.Error); @@ -74,17 +76,17 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'third line', severity.Info); elements = repl.getReplElements(); assert.equal(elements.length, 3); - assert.equal(elements[0], 'first line\n'); + assert.equal(elements[0].value, 'first line\n'); assert.equal(elements[0].count, 3); - assert.equal(elements[1], 'second line'); + assert.equal(elements[1].value, 'second line'); assert.equal(elements[1].count, 2); - assert.equal(elements[2], 'third line'); + assert.equal(elements[2].value, 'third line'); assert.equal(elements[2].count, 1); }); test('repl output count', () => { const session = createMockSession(model); - const repl = new ReplModel(); + const repl = new ReplModel(configurationService); repl.appendToRepl(session, 'first line\n', severity.Info); repl.appendToRepl(session, 'first line\n', severity.Info); repl.appendToRepl(session, 'first line\n', severity.Info); @@ -93,11 +95,13 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'third line', severity.Info); const elements = repl.getReplElements(); assert.equal(elements.length, 3); - assert.equal(elements[0], 'first line\n'); + assert.equal(elements[0].value, 'first line\n'); + assert.equal(elements[0].toString(), 'first line\nfirst line\nfirst line\n'); assert.equal(elements[0].count, 3); - assert.equal(elements[1], 'second line'); + assert.equal(elements[1].value, 'second line'); + assert.equal(elements[1].toString(), 'second line\nsecond line'); assert.equal(elements[1].count, 2); - assert.equal(elements[2], 'third line'); + assert.equal(elements[2].value, 'third line'); assert.equal(elements[2].count, 1); }); @@ -153,7 +157,7 @@ suite('Debug - REPL', () => { session['raw'] = rawSession; const thread = new Thread(session, 'mockthread', 1); const stackFrame = new StackFrame(thread, 1, undefined, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - const replModel = new ReplModel(); + const replModel = new ReplModel(configurationService); replModel.addReplExpression(session, stackFrame, 'myVariable').then(); replModel.addReplExpression(session, stackFrame, 'myVariable').then(); replModel.addReplExpression(session, stackFrame, 'myVariable').then(); @@ -172,7 +176,7 @@ suite('Debug - REPL', () => { model.addSession(session); const adapter = new MockDebugAdapter(); - const raw = new RawDebugSession(adapter, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const raw = new RawDebugSession(adapter, undefined!, '', undefined!, undefined!, undefined!, undefined!, undefined!); session.initializeForTest(raw); await session.addReplExpression(undefined, 'before.1'); @@ -191,7 +195,7 @@ suite('Debug - REPL', () => { test('repl groups', async () => { const session = createMockSession(model); - const repl = new ReplModel(); + const repl = new ReplModel(configurationService); repl.appendToRepl(session, 'first global line', severity.Info); repl.startGroup('group_1', true); @@ -229,7 +233,7 @@ suite('Debug - REPL', () => { test('repl filter', async () => { const session = createMockSession(model); - const repl = new ReplModel(); + const repl = new ReplModel(configurationService); const replFilter = new ReplFilter(); const getFilteredElements = () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/telemetry.test.ts b/src/vs/workbench/contrib/debug/test/browser/telemetry.test.ts index 18bb41d83e..e36149ccb7 100644 --- a/src/vs/workbench/contrib/debug/test/browser/telemetry.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/telemetry.test.ts @@ -27,7 +27,7 @@ suite('Debug - DebugSession telemetry', () => { const telemetryService = telemetry as Partial as ITelemetryService; session = new DebugSession(generateUuid(), undefined!, undefined!, model, undefined, undefined!, telemetryService, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); - session.initializeForTest(new RawDebugSession(adapter, undefined!, undefined!, telemetryService, undefined!, undefined!, undefined!)); + session.initializeForTest(new RawDebugSession(adapter, undefined!, '', undefined!, telemetryService, undefined!, undefined!, undefined!)); }); test('does not send telemetry when opted out', async () => { diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 0d651cc24a..efbdbfaf1f 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -11,7 +11,7 @@ import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { URI } from 'vs/base/common/uri'; import { ExecutableDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/modelService.test'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 6df926c006..c691daace9 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -27,7 +27,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; -import { renderCodicons } from 'vs/base/browser/codicons'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -297,21 +297,28 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { const activationId = activationTimes.activationReason.extensionId.value; const activationEvent = activationTimes.activationReason.activationEvent; if (activationEvent === '*') { - title = nls.localize('starActivation', "Activated by {0} on start-up", activationId); + title = nls.localize({ + key: 'starActivation', + comment: [ + '{0} will be an extension identifier' + ] + }, "Activated by {0} on start-up", activationId); } else if (/^workspaceContains:/.test(activationEvent)) { let fileNameOrGlob = activationEvent.substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { title = nls.localize({ key: 'workspaceContainsGlobActivation', comment: [ - '{0} will be a glob pattern' + '{0} will be a glob pattern', + '{1} will be an extension identifier' ] - }, "Activated by {1} because a file matching {1} exists in your workspace", fileNameOrGlob, activationId); + }, "Activated by {1} because a file matching {0} exists in your workspace", fileNameOrGlob, activationId); } else { title = nls.localize({ key: 'workspaceContainsFileActivation', comment: [ - '{0} will be a file name' + '{0} will be a file name', + '{1} will be an extension identifier' ] }, "Activated by {1} because file {0} exists in your workspace", fileNameOrGlob, activationId); } @@ -320,7 +327,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { title = nls.localize({ key: 'workspaceContainsTimeout', comment: [ - '{0} will be a glob pattern' + '{0} will be a glob pattern', + '{1} will be an extension identifier' ] }, "Activated by {1} because searching for {0} took too long", glob, activationId); } else if (activationEvent === 'onStartupFinished') { @@ -337,7 +345,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { title = nls.localize({ key: 'workspaceGenericActivation', comment: [ - 'The {0} placeholder will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.' + '{0} will be an activation event, like e.g. \'language:typescript\', \'debug\', etc.', + '{1} will be an extension identifier' ] }, "Activated by {1} on {0}", activationEvent, activationId); } @@ -346,28 +355,28 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { clearNode(data.msgContainer); if (this._getUnresponsiveProfile(element.description.identifier)) { - const el = $('span', undefined, ...renderCodicons(` $(alert) Unresponsive`)); + const el = $('span', undefined, ...renderLabelWithIcons(` $(alert) Unresponsive`)); el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); data.msgContainer.appendChild(el); } if (isNonEmptyArray(element.status.runtimeErrors)) { - const el = $('span', undefined, ...renderCodicons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); data.msgContainer.appendChild(el); } if (element.status.messages && element.status.messages.length > 0) { - const el = $('span', undefined, ...renderCodicons(`$(alert) ${element.status.messages[0].message}`)); + const el = $('span', undefined, ...renderLabelWithIcons(`$(alert) ${element.status.messages[0].message}`)); data.msgContainer.appendChild(el); } - if (element.description.extensionLocation.scheme !== Schemas.file) { - const el = $('span', undefined, ...renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`)); + if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) { + const el = $('span', undefined, ...renderLabelWithIcons(`$(remote) ${element.description.extensionLocation.authority}`)); data.msgContainer.appendChild(el); const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority); if (hostLabel) { - reset(el, ...renderCodicons(`$(remote) ${hostLabel}`)); + reset(el, ...renderLabelWithIcons(`$(remote) ${hostLabel}`)); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 5b4f6def46..e2c96a7522 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -31,7 +31,7 @@ import { UpdateAction, ReloadAction, MaliciousStatusLabelAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, - InstallAnotherVersionAction, ExtensionEditorManageExtensionAction + InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -66,22 +66,14 @@ import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/to import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { insane } from 'vs/base/common/insane/insane'; function removeEmbeddedSVGs(documentContent: string): string { - const newDocument = new DOMParser().parseFromString(documentContent, 'text/html'); - - // remove all inline svgs - const allSVGs = newDocument.documentElement.querySelectorAll('svg'); - if (allSVGs) { - for (let i = 0; i < allSVGs.length; i++) { - const svg = allSVGs[i]; - if (svg.parentNode) { - svg.parentNode.removeChild(allSVGs[i]); - } + return insane(documentContent, { + filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { + return token.tag !== 'svg'; } - } - - return newDocument.documentElement.outerHTML; + }); } class NavBar extends Disposable { @@ -170,6 +162,11 @@ interface IExtensionEditorTemplate { header: HTMLElement; } +const enum WebviewIndex { + Readme, + Changelog +} + export class ExtensionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.extension'; @@ -180,6 +177,12 @@ export class ExtensionEditor extends EditorPane { private extensionChangelog: Cache | null; private extensionManifest: Cache | null; + // Some action bar items use a webview whose vertical scroll position we track in this map + private initialScrollProgress: Map = new Map(); + + // Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed + private currentIdentifier: string = ''; + private layoutParticipants: ILayoutParticipant[] = []; private readonly contentDisposables = this._register(new DisposableStore()); private readonly transientDisposables = this._register(new DisposableStore()); @@ -338,6 +341,11 @@ export class ExtensionEditor extends EditorPane { this.editorLoadComplete = false; const extension = input.extension; + if (this.currentIdentifier !== extension.identifier.id) { + this.initialScrollProgress.clear(); + this.currentIdentifier = extension.identifier.id; + } + this.transientDisposables.clear(); this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token))); @@ -444,6 +452,7 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(DisableDropDownAction), this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), + this.instantiationService.createInstance(WebInstallAction), combinedInstallAction, this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [ @@ -584,7 +593,7 @@ export class ExtensionEditor extends EditorPane { return Promise.resolve(null); } - private async openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + private async openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate, webviewIndex: WebviewIndex, token: CancellationToken): Promise { try { const body = await this.renderMarkdown(cacheResult, template); if (token.isCancellationRequested) { @@ -593,15 +602,22 @@ export class ExtensionEditor extends EditorPane { const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', { enableFindWidget: true, + tryRestoreScrollPosition: true, }, {}, undefined)); + webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0; + webview.claim(this, this.scopedContextKeyService); setParentFlowTo(webview.container, template.content); webview.layoutWebviewOverElement(template.content); webview.html = body; + webview.claim(this, undefined); this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + + this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: () => { webview.layoutWebviewOverElement(template.content); @@ -644,8 +660,8 @@ export class ExtensionEditor extends EditorPane { private async renderMarkdown(cacheResult: CacheResult, template: IExtensionEditorTemplate) { const contents = await this.loadContents(() => cacheResult, template); const content = await renderMarkdownDocument(contents, this.extensionService, this.modeService); - const documentContent = await this.renderBody(content); - return removeEmbeddedSVGs(documentContent); + const sanitizedContent = removeEmbeddedSVGs(content); + return await this.renderBody(sanitizedContent); } private async renderBody(body: string): Promise { @@ -731,9 +747,16 @@ export class ExtensionEditor extends EditorPane { } code { + font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace; + font-size: 14px; + line-height: 19px; + } + + pre code { font-family: var(--vscode-editor-font-family); font-weight: var(--vscode-editor-font-weight); font-size: var(--vscode-editor-font-size); + line-height: 1.5; } code > div { @@ -845,7 +868,7 @@ export class ExtensionEditor extends EditorPane { if (manifest && manifest.extensionPack && manifest.extensionPack.length) { return this.openExtensionPackReadme(manifest, template, token); } - return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, token); + return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template, WebviewIndex.Readme, token); } private async openExtensionPackReadme(manifest: IExtensionManifest, template: IExtensionEditorTemplate, token: CancellationToken): Promise { @@ -877,14 +900,14 @@ export class ExtensionEditor extends EditorPane { await Promise.all([ this.renderExtensionPack(manifest, extensionPackContent, token), - this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, token), + this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), { ...template, ...{ content: readmeContent } }, WebviewIndex.Readme, token), ]); return { focus: () => extensionPackContent.focus() }; } private openChangelog(template: IExtensionEditorTemplate, token: CancellationToken): Promise { - return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, token); + return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template, WebviewIndex.Changelog, token); } private openContributions(template: IExtensionEditorTemplate, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index dfa2bdbfea..3fe28bc335 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -5,7 +5,7 @@ import { IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise, raceCancellablePromises, raceCancellation, timeout } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -13,7 +13,7 @@ import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -21,6 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; import { EnablementState, IWorkbenchExtensioManagementService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; @@ -28,6 +29,7 @@ import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/e type ExtensionRecommendationsNotificationClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; type ExtensionWorkspaceRecommendationsNotificationClassification = { @@ -135,6 +137,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { this.tasExperimentService = tasExperimentService; @@ -153,13 +156,13 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec } return this.promptRecommendationsNotification(extensionIds, message, searchValue, source, { - onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id })), - onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id })), - onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id })), + onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), + onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), + onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) })), onDidNeverShowRecommendedExtensionsAgain: (extensions: IExtension[]) => { for (const extension of extensions) { this.addToImportantRecommendationsIgnore(extension.identifier.id); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id }); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string, source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id, source: RecommendationSourceToString(source) }); } this.notificationService.prompt( Severity.Info, @@ -207,6 +210,11 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec return RecommendationsNotificationResult.Ignored; } + // Do not show exe based recommendations in remote window + if (source === RecommendationSource.EXE && this.workbenchEnvironmentService.remoteAuthority) { + return RecommendationsNotificationResult.IncompatibleWindow; + } + // Ignore exe recommendation if the window // => has shown an exe based recommendation already // => or has shown any two recommendations already @@ -245,8 +253,8 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec const installExtensions = async (isMachineScoped?: boolean) => { this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); onDidInstallRecommendedExtensions(extensions); - await Promise.all([ - Promise.all(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), + await Promises.settled([ + Promises.settled(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), this.extensionManagementService.installExtensions(extensions.map(e => e.gallery!), { isMachineScoped }) ]); }; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 43419078c0..6442d5433d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,20 +6,16 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, SyncActionDescriptor, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { - OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, - ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, RefreshExtensionsAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; @@ -37,12 +33,10 @@ import { ExtensionActivationProgress } from 'vs/workbench/contrib/extensions/bro import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionDependencyChecker } from 'vs/workbench/contrib/extensions/browser/extensionsDependencyChecker'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionsPolicy } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} -import { RemoteExtensionsInstaller } from 'vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller'; -import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsService } from 'vs/workbench/common/views'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { ContextKeyAndExpr, ContextKeyExpr, ContextKeyOrExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyOrExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { InstallExtensionQuickAccessProvider, ManageExtensionsQuickAccessProvider } from 'vs/workbench/contrib/extensions/browser/extensionsQuickAccess'; @@ -66,7 +60,15 @@ import { IAction } from 'vs/base/common/actions'; import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { Schemas } from 'vs/base/common/network'; import { ShowRuntimeExtensionsAction } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; -import { extensionsViewIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { clearSearchResultsIcon, configureRecommendedIcon, extensionsViewIcon, filterIcon, installWorkspaceRecommendedIcon, refreshIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { ExtensionsPolicy, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { isArray } from 'vs/base/common/types'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; +import { Promises } from 'vs/base/common/async'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -84,16 +86,6 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions"), needsEditor: false }] }); -// Explorer -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: 'extensions', - command: { - id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, - title: localize('installVSIX', "Install Extension VSIX"), - }, - when: ResourceContextKey.Extension.isEqualTo('.vsix') -}); - // Editor Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -109,12 +101,18 @@ Registry.as(EditorExtensions.Editors).registerEditor( Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer( { id: VIEWLET_ID, - name: localize('extensions', "Extensions"), + title: localize('extensions', "Extensions"), + openCommandActionDescriptor: { + id: VIEWLET_ID, + mnemonicTitle: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X }, + order: 4, + }, ctorDescriptor: new SyncDescriptor(ExtensionsViewPaneContainer), icon: extensionsViewIcon, order: 14, // {{SQL CARBON EDIT}} rejectAddedViews: true, - alwaysUseContainerInfo: true + alwaysUseContainerInfo: true, }, ViewContainerLocation.Sidebar); @@ -233,36 +231,6 @@ CommandsRegistry.registerCommand({ } }); -CommandsRegistry.registerCommand({ - id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, - handler: async (accessor: ServicesAccessor, resources: URI[] | URI) => { - const extensionService = accessor.get(IExtensionService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const hostService = accessor.get(IHostService); - const notificationService = accessor.get(INotificationService); - - const extensions = Array.isArray(resources) ? resources : [resources]; - await Promise.all(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) - .then(async (extensions) => { - for (const extension of extensions) { - const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) - : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => hostService.reload() - }] : []; - notificationService.prompt( - Severity.Info, - message, - actions, - { sticky: true } - ); - } - }); - } -}); - CommandsRegistry.registerCommand({ id: 'workbench.extensions.uninstallExtension', description: { @@ -323,57 +291,6 @@ CommandsRegistry.registerCommand({ } }); -// File menu registration - -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '2_keybindings', - command: { - id: ShowRecommendedKeymapExtensionsAction.ID, - title: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_keybindings', - command: { - id: ShowRecommendedKeymapExtensionsAction.ID, - title: localize('miOpenKeymapExtensions2', "Keymaps") - }, - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: VIEWLET_ID, - title: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions") - }, - order: 3 -}); - -// View menu - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_views', - command: { - id: VIEWLET_ID, - title: localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") - }, - order: 5 -}); - -// Global Activity Menu - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_configuration', - command: { - id: VIEWLET_ID, - title: localize('showExtensions', "Extensions") - }, - order: 3 -}); - function overrideActionForActiveExtensionEditorWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) { command?.addImplementation(105, (accessor) => { const editorService = accessor.get(IEditorService); @@ -406,13 +323,25 @@ async function runAction(action: IAction): Promise { } } -class ExtensionsContributions implements IWorkbenchContribution { +interface IExtensionActionOptions extends IAction2Options { + menuTitles?: { [id: number]: string }; + run(accessor: ServicesAccessor, ...args: any[]): Promise; +} + +class ExtensionsContributions extends Disposable implements IWorkbenchContribution { constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IContextKeyService contextKeyService: IContextKeyService, + @IViewletService private readonly viewletService: IViewletService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotificationService private readonly notificationService: INotificationService, + @ICommandService private readonly commandService: ICommandService, ) { + super(); const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService); if (extensionGalleryService.isEnabled()) { hasGalleryContext.set(true); @@ -454,437 +383,659 @@ class ExtensionsContributions implements IWorkbenchContribution { // Global actions private registerGlobalActions(): void { - registerAction2(class extends Action2 { - constructor() { - super({ - id: OpenExtensionsViewletAction.ID, - title: { value: OpenExtensionsViewletAction.LABEL, original: 'Show Extensions' }, - category: CATEGORIES.View, - menu: { - id: MenuId.CommandPalette, - }, - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X, - weight: KeybindingWeight.WorkbenchContrib - } - }); + this._register(MenuRegistry.appendMenuItems([{ + id: MenuId.MenubarPreferencesMenu, + item: { + command: { + id: VIEWLET_ID, + title: localize({ key: 'miPreferencesExtensions', comment: ['&& denotes a mnemonic'] }, "&&Extensions") + }, + group: '1_settings', + order: 3 } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(OpenExtensionsViewletAction, OpenExtensionsViewletAction.ID, OpenExtensionsViewletAction.LABEL)); + }, { + id: MenuId.GlobalActivity, + item: { + command: { + id: VIEWLET_ID, + title: localize('showExtensions', "Extensions") + }, + group: '2_configuration', + order: 3 + } + }])); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.installExtensions', + title: { value: localize('installExtensions', "Install Extensions"), original: 'Install Extensions' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async (accessor: ServicesAccessor) => { + accessor.get(IViewsService).openViewContainer(VIEWLET_ID); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallExtensionsAction.ID, - title: { value: InstallExtensionsAction.LABEL, original: 'Install Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + this.registerExtensionAction({ + id: 'workbench.extensions.action.showRecommendedKeymapExtensions', + title: { value: localize('showRecommendedKeymapExtensionsShort', "Keymaps"), original: 'Keymaps' }, + category: PreferencesLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: MenuId.MenubarPreferencesMenu, + group: '2_keybindings', + order: 2 + }, { + id: MenuId.GlobalActivity, + group: '2_keybindings', + order: 2 + }], + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M), + weight: KeybindingWeight.WorkbenchContrib + }, + menuTitles: { + [MenuId.MenubarPreferencesMenu.id]: localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymaps"), + [MenuId.GlobalActivity.id]: localize('miOpenKeymapExtensions2', "Keymaps") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:keymaps ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showLanguageExtensions', + title: { value: localize('showLanguageExtensionsShort', "Language Extensions"), original: 'Language Extensions' }, + category: PreferencesLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@category:"programming languages" @sort:installs ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.checkForUpdates', + title: { value: localize('checkForUpdates', "Check for Extension Updates"), original: 'Check for Extension Updates' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '1_updates', + order: 1 + }], + run: async () => { + await this.extensionsWorkbenchService.checkForUpdates(); + const outdated = this.extensionsWorkbenchService.outdated; + if (!outdated.length) { + this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); + return; + } + + let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); + + const disabledExtensionsCount = outdated.filter(ext => ext.local && !this.extensionEnablementService.isEnabled(ext.local)).length; + if (disabledExtensionsCount) { + if (outdated.length === 1) { + msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); + } else if (disabledExtensionsCount === 1) { + msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); + } else if (disabledExtensionsCount === outdated.length) { + msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); + } else { + msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallExtensionsAction, InstallExtensionsAction.ID, InstallExtensionsAction.LABEL)); + } + + this.viewletService.openViewlet(VIEWLET_ID, true); + this.notificationService.info(msgAvailableExtensions); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowOutdatedExtensionsAction.ID, - title: { value: ShowOutdatedExtensionsAction.LABEL, original: 'Show Outdated Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAutoUpdate', + title: { value: localize('disableAutoUpdate', "Disable Auto Updating Extensions"), original: 'Disable Auto Updating Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`)]), + group: '1_updates', + order: 2 + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, false) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.updateAllExtensions', + title: { value: localize('updateAll', "Update All Extensions"), original: 'Update All Extensions' }, + category: ExtensionsLocalizedLabel, + precondition: HasOutdatedExtensionsContext, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`).negate()]), + group: '1_updates', + order: 2 + }], + run: () => { + return Promise.all(this.extensionsWorkbenchService.outdated.map(async extension => { + try { + await this.extensionsWorkbenchService.install(extension); + } catch (err) { + runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err)); } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL)); + })); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowRecommendedExtensionsAction.ID, - title: { value: ShowRecommendedExtensionsAction.LABEL, original: 'Show Recommended Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAutoUpdate', + title: { value: localize('enableAutoUpdate', "Enable Auto Updating Extensions"), original: 'Enable Auto Updating Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyDefinedExpr.create(`config.${AutoUpdateConfigurationKey}`).negate()]), + group: '1_updates', + order: 3 + }], + run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, true) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAll', + title: { value: localize('enableAll', "Enable All Extensions"), original: 'Enable All Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '2_enablement', + order: 1 + }], + run: async () => { + const extensionsToEnable = this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); + if (extensionsToEnable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToEnable, EnablementState.EnabledGlobally); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowRecommendedKeymapExtensionsAction.ID, - title: { value: ShowRecommendedKeymapExtensionsAction.LABEL, original: 'Keymaps' }, - category: PreferencesLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - }, - keybinding: { - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M), - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowRecommendedKeymapExtensionsAction, ShowRecommendedKeymapExtensionsAction.ID, ShowRecommendedKeymapExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.enableAllWorkspace', + title: { value: localize('enableAllWorkspace', "Enable All Extensions for this Workspace"), original: 'Enable All Extensions for this Workspace' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async () => { + const extensionsToEnable = this.extensionsWorkbenchService.local.filter(e => !!e.local && this.extensionEnablementService.canChangeEnablement(e.local) && !this.extensionEnablementService.isEnabled(e.local)); + if (extensionsToEnable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToEnable, EnablementState.EnabledWorkspace); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowLanguageExtensionsAction.ID, - title: { value: ShowLanguageExtensionsAction.LABEL, original: 'Language Extensions' }, - category: PreferencesLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowLanguageExtensionsAction, ShowLanguageExtensionsAction.ID, ShowLanguageExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAll', + title: { value: localize('disableAll', "Disable All Installed Extensions"), original: 'Disable All Installed Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: '2_enablement', + order: 2 + }], + run: async () => { + const extensionsToDisable = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); + if (extensionsToDisable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToDisable, EnablementState.DisabledGlobally); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowPopularExtensionsAction.ID, - title: { value: ShowPopularExtensionsAction.LABEL, original: 'Show Popular Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.disableAllWorkspace', + title: { value: localize('disableAllWorkspace', "Disable All Installed Extensions for this Workspace"), original: 'Disable All Installed Extensions for this Workspace' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: async () => { + const extensionsToDisable = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && !!e.local && this.extensionEnablementService.isEnabled(e.local) && this.extensionEnablementService.canChangeEnablement(e.local)); + if (extensionsToDisable.length) { + await this.extensionsWorkbenchService.setEnablement(extensionsToDisable, EnablementState.DisabledWorkspace); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowEnabledExtensionsAction.ID, - title: { value: ShowEnabledExtensionsAction.LABEL, original: 'Show Enabled Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } + this.registerExtensionAction({ + id: SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, + title: { value: localize('InstallFromVSIX', "Install from VSIX..."), original: 'Install from VSIX...' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER]) + }, { + id: MenuId.ViewContainerTitle, + when: ContextKeyAndExpr.create([ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]), + group: '3_install', + order: 1 + }], + run: async (accessor: ServicesAccessor) => { + const fileDialogService = accessor.get(IFileDialogService); + const commandService = accessor.get(ICommandService); + const vsixPaths = await fileDialogService.showOpenDialog({ + title: localize('installFromVSIX', "Install from VSIX"), + filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], + canSelectFiles: true, + canSelectMany: true, + openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, ShowEnabledExtensionsAction.LABEL)); + if (vsixPaths) { + await commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixPaths); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowInstalledExtensionsAction.ID, - title: { value: ShowInstalledExtensionsAction.LABEL, original: 'Show Installed Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, + title: localize('installVSIX', "Install Extension VSIX"), + menu: [{ + id: MenuId.ExplorerContext, + group: 'extensions', + when: ContextKeyAndExpr.create([ResourceContextKey.Extension.isEqualTo('.vsix'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]), + }], + run: async (accessor: ServicesAccessor, resources: URI[] | URI) => { + const extensionService = accessor.get(IExtensionService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const hostService = accessor.get(IHostService); + const notificationService = accessor.get(INotificationService); + + const extensions = Array.isArray(resources) ? resources : [resources]; + await Promises.settled(extensions.map(async (vsix) => await extensionsWorkbenchService.install(vsix))) + .then(async (extensions) => { + for (const extension of extensions) { + const requireReload = !(extension.local && extensionService.canAddExtension(toExtensionDescription(extension.local))); + const message = requireReload ? localize('InstallVSIXAction.successReload', "Completed installing {0} extension from VSIX. Please reload Visual Studio Code to enable it.", extension.displayName || extension.name) + : localize('InstallVSIXAction.success', "Completed installing {0} extension from VSIX.", extension.displayName || extension.name); + const actions = requireReload ? [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => hostService.reload() + }] : []; + notificationService.prompt( + Severity.Info, + message, + actions + ); + } + }); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowDisabledExtensionsAction.ID, - title: { value: ShowDisabledExtensionsAction.LABEL, original: 'Show Disabled Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL)); + const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); + MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + submenu: extensionsFilterSubMenu, + title: localize('filterExtensions', "Filter Extensions..."), + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 1, + icon: filterIcon, + }); + + const showFeaturedExtensionsId = 'extensions.filter.featured'; + this.registerExtensionAction({ + id: showFeaturedExtensionsId, + title: { value: localize('showFeaturedExtensions', "Show Featured Extensions"), original: 'Show Featured Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 1, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('featured filter', "Featured") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@featured ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showPopularExtensions', + title: { value: localize('showPopularExtensions', "Show Popular Extensions"), original: 'Show Popular Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('most popular filter', "Most Popular") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@popular ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showRecommendedExtensions', + title: { value: localize('showRecommendedExtensions', "Show Recommended Extensions"), original: 'Show Recommended Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('most popular recommended', "Recommended") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.recentlyPublishedExtensions', + title: { value: localize('recentlyPublishedExtensions', "Show Recently Published Extensions"), original: 'Show Recently Published Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: CONTEXT_HAS_GALLERY + }, { + id: extensionsFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + group: '1_predefined', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('recently published filter', "Recently Published") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@sort:publishedDate ')) + }); + + const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + submenu: extensionsCategoryFilterSubMenu, + title: localize('filter by category', "Category"), + when: CONTEXT_HAS_GALLERY, + group: '2_categories', + order: 1, + }); + + EXTENSION_CATEGORIES.map((category, index) => { + this.registerExtensionAction({ + id: `extensions.actions.searchByCategory.${category}`, + title: category, + menu: [{ + id: extensionsCategoryFilterSubMenu, + when: CONTEXT_HAS_GALLERY, + order: index, + }], + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, `@category:"${category.toLowerCase()}"`)) + }); + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.listBuiltInExtensions', + title: { value: localize('showBuiltInExtensions', "Show Built-in Extensions"), original: 'Show Built-in Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 1, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('builtin filter', "Built-in") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@builtin ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showInstalledExtensions', + title: { value: localize('showInstalledExtensions', "Show Installed Extensions"), original: 'Show Installed Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 2, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('installed filter', "Installed") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@installed ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showEnabledExtensions', + title: { value: localize('showEnabledExtensions', "Show Enabled Extensions"), original: 'Show Enabled Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 3, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('enabled filter', "Enabled") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@enabled ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.showDisabledExtensions', + title: { value: localize('showDisabledExtensions', "Show Disabled Extensions"), original: 'Show Disabled Extensions' }, + category: ExtensionsLocalizedLabel, + + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 4, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('disabled filter', "Disabled") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@disabled ')) + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.listOutdatedExtensions', + title: { value: localize('showOutdatedExtensions', "Show Outdated Extensions"), original: 'Show Outdated Extensions' }, + category: ExtensionsLocalizedLabel, + menu: [{ + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, { + id: extensionsFilterSubMenu, + group: '3_installed', + order: 5, + }], + menuTitles: { + [extensionsFilterSubMenu.id]: localize('outdated filter', "Outdated") + }, + run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@outdated ')) + }); + + const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + submenu: extensionsSortSubMenu, + title: localize('sorty by', "Sort By"), + when: CONTEXT_HAS_GALLERY, + group: '4_sort', + order: 1, + }); + + [ + { id: 'installs', title: localize('sort by installs', "Install Count") }, + { id: 'rating', title: localize('sort by rating', "Rating") }, + { id: 'name', title: localize('sort by name', "Name") }, + { id: 'publishedDate', title: localize('sort by date', "Published Date") }, + ].map(({ id, title }, index) => { + this.registerExtensionAction({ + id: `extensions.sort.${id}`, + title, + precondition: DefaultViewsContext.toNegated(), + menu: [{ + id: extensionsSortSubMenu, + when: CONTEXT_HAS_GALLERY, + order: index, + }], + toggled: ExtensionsSortByContext.isEqualTo(id), + run: async () => { + const viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true); + const extensionsViewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; + const currentQuery = Query.parse(extensionsViewPaneContainer.searchValue || ''); + extensionsViewPaneContainer.search(new Query(currentQuery.value, id, currentQuery.groupBy).toString()); + extensionsViewPaneContainer.focus(); + } + }); + }); + + this.registerExtensionAction({ + id: 'workbench.extensions.action.clearExtensionsSearchResults', + title: { value: localize('clearExtensionsSearchResults', "Clear Extensions Search Results"), original: 'Clear Extensions Search Results' }, + category: ExtensionsLocalizedLabel, + icon: clearSearchResultsIcon, + f1: true, + precondition: DefaultViewsContext.toNegated(), + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 3, + }, + run: async (accessor: ServicesAccessor) => { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(VIEWLET_ID); + if (viewPaneContainer) { + const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; + extensionsViewPaneContainer.search(''); + extensionsViewPaneContainer.focus(); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: ShowBuiltInExtensionsAction.ID, - title: { value: ShowBuiltInExtensionsAction.LABEL, original: 'Show Built-in Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.refreshExtension', + title: { value: localize('refreshExtension', "Refresh"), original: 'Refresh' }, + category: ExtensionsLocalizedLabel, + icon: refreshIcon, + f1: true, + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyEqualsExpr.create('viewContainer', VIEWLET_ID), + group: 'navigation', + order: 2 + }, + run: async (accessor: ServicesAccessor) => { + const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(VIEWLET_ID); + if (viewPaneContainer) { + await (viewPaneContainer as IExtensionsViewPaneContainer).refresh(); + } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: UpdateAllAction.ID, - title: { value: UpdateAllAction.LABEL, original: 'Update All Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false)); + this.registerExtensionAction({ + id: 'workbench.extensions.action.installWorkspaceRecommendedExtensions', + title: localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), + icon: installWorkspaceRecommendedIcon, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', WORKSPACE_RECOMMENDATIONS_VIEW_ID), + group: 'navigation', + order: 1 + }, + run: async (accessor: ServicesAccessor) => { + const view = accessor.get(IViewsService).getActiveViewWithId(WORKSPACE_RECOMMENDATIONS_VIEW_ID) as IWorkspaceRecommendedExtensionsView; + return view.installWorkspaceRecommendations(); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallVSIXAction.ID, - title: { value: InstallVSIXAction.LABEL, original: 'Install from VSIX...' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); - } + this.registerExtensionAction({ + id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, + title: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, + icon: configureRecommendedIcon, + menu: [{ + id: MenuId.CommandPalette, + when: WorkbenchStateContext.notEqualsTo('empty'), + }, { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', WORKSPACE_RECOMMENDATIONS_VIEW_ID), + group: 'navigation', + order: 2 + }], + run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)) }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAllAction.ID, - title: { value: DisableAllAction.LABEL, original: 'Disable All Installed Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false)); - } + this.registerExtensionAction({ + id: InstallSpecificVersionOfExtensionAction.ID, + title: { value: InstallSpecificVersionOfExtensionAction.LABEL, original: 'Install Specific Version of Extension...' }, + category: ExtensionsLocalizedLabel, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL)) }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAllWorkspaceAction.ID, - title: { value: DisableAllWorkspaceAction.LABEL, original: 'Disable All Installed Extensions for this Workspace' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAllWorkspaceAction, DisableAllWorkspaceAction.ID, DisableAllWorkspaceAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAllAction.ID, - title: { value: EnableAllAction.LABEL, original: 'Enable All Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAllWorkspaceAction.ID, - title: { value: EnableAllWorkspaceAction.LABEL, original: 'Enable All Extensions for this Workspace' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([WorkbenchStateContext.notEqualsTo('empty'), ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAllWorkspaceAction, EnableAllWorkspaceAction.ID, EnableAllWorkspaceAction.LABEL, false)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: CheckForUpdatesAction.ID, - title: { value: CheckForUpdatesAction.LABEL, original: 'Check for Extension Updates' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: ClearExtensionsSearchResultsAction.ID, - title: { value: ClearExtensionsSearchResultsAction.LABEL, original: 'Clear Extensions Search Results' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ClearExtensionsSearchResultsAction, ClearExtensionsSearchResultsAction.ID, ClearExtensionsSearchResultsAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: RefreshExtensionsAction.ID, - title: { value: RefreshExtensionsAction.LABEL, original: 'Refresh' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: EnableAutoUpdateAction.ID, - title: { value: EnableAutoUpdateAction.LABEL, original: 'Enable Auto Updating Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: DisableAutoUpdateAction.ID, - title: { value: DisableAutoUpdateAction.LABEL, original: 'Disable Auto Updating Extensions' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: InstallSpecificVersionOfExtensionAction.ID, - title: { value: InstallSpecificVersionOfExtensionAction.LABEL, original: 'Install Specific Version of Extension...' }, - category: ExtensionsLocalizedLabel, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL)); - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: ReinstallAction.ID, - title: { value: ReinstallAction.LABEL, original: 'Reinstall Extension...' }, - category: CATEGORIES.Developer, - menu: { - id: MenuId.CommandPalette, - when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]) - } - }); - } - run(accessor: ServicesAccessor) { - return runAction(accessor.get(IInstantiationService).createInstance(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL)); - } + this.registerExtensionAction({ + id: ReinstallAction.ID, + title: { value: ReinstallAction.LABEL, original: 'Reinstall Extension...' }, + category: CATEGORIES.Developer, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyAndExpr.create([CONTEXT_HAS_GALLERY, ContextKeyOrExpr.create([CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER])]) + }, + run: () => runAction(this.instantiationService.createInstance(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL)) }); } // Extension Context Menu private registerContextMenuActions(): void { - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.copyExtension', - title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, - menu: { - id: MenuId.ExtensionContext, - group: '1_copy' - } - }); - } - - async run(accessor: ServicesAccessor, extensionId: string) { - const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); - let extension = extensionWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] - || (await extensionWorkbenchService.queryGallery({ names: [extensionId], pageSize: 1 }, CancellationToken.None)).firstPage[0]; + this.registerExtensionAction({ + id: 'workbench.extensions.action.copyExtension', + title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + }, + run: async (accessor: ServicesAccessor, extensionId: string) => { + const clipboardService = accessor.get(IClipboardService); + let extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await this.extensionsWorkbenchService.queryGallery({ names: [extensionId], pageSize: 1 }, CancellationToken.None)).firstPage[0]; if (extension) { const name = localize('extensionInfoName', 'Name: {0}', extension.displayName); const id = localize('extensionInfoId', 'Id: {0}', extensionId); @@ -893,165 +1044,104 @@ class ExtensionsContributions implements IWorkbenchContribution { const publisher = localize('extensionInfoPublisher', 'Publisher: {0}', extension.publisherDisplayName); const link = extension.url ? localize('extensionInfoVSMarketplaceLink', 'VS Marketplace Link: {0}', `${extension.url}`) : null; const clipboardStr = `${name}\n${id}\n${description}\n${verision}\n${publisher}${link ? '\n' + link : ''}`; - await accessor.get(IClipboardService).writeText(clipboardStr); + await clipboardService.writeText(clipboardStr); } } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.copyExtensionId', - title: { value: localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"), original: 'Copy Extension Id' }, - menu: { - id: MenuId.ExtensionContext, - group: '1_copy' - } - }); - } - - async run(accessor: ServicesAccessor, id: string) { - await accessor.get(IClipboardService).writeText(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.copyExtensionId', + title: { value: localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"), original: 'Copy Extension Id' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IClipboardService).writeText(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.configure', - title: { value: localize('workbench.extensions.action.configure', "Extension Settings"), original: 'Extension Settings' }, - menu: { - id: MenuId.ExtensionContext, - group: '2_configure', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) - } - }); - } - - async run(accessor: ServicesAccessor, id: string) { - await accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.configure', + title: { value: localize('workbench.extensions.action.configure', "Extension Settings"), original: 'Extension Settings' }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, - title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, - menu: { - id: MenuId.ExtensionContext, - group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) - }, - }); - } - - async run(accessor: ServicesAccessor, id: string) { - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const extension = extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); + this.registerExtensionAction({ + id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, + title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()) + }, + run: async (accessor: ServicesAccessor, id: string) => { + const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); if (extension) { - return extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); + return this.extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); } } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.ignoreRecommendation', - title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.has('isExtensionRecommended'), - order: 1 - }, - }); - } - - async run(accessor: ServicesAccessor, id: string): Promise { - accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.ignoreRecommendation', + title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isExtensionRecommended'), + order: 1 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.undoIgnoredRecommendation', - title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.has('isUserIgnoredRecommendation'), - order: 1 - }, - }); - } - - async run(accessor: ServicesAccessor, id: string): Promise { - accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.undoIgnoredRecommendation', + title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.has('isUserIgnoredRecommendation'), + order: 1 + }, + run: async (accessor: ServicesAccessor, id: string) => accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), - order: 2 - }, - }); - } - - run(accessor: ServicesAccessor, id: string): Promise { - return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()), + order: 2 + }, + run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, - menu: { - id: MenuId.ExtensionContext, - group: '3_recommendations', - when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), - order: 2 - }, - }); - } - - run(accessor: ServicesAccessor, id: string): Promise { - return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` }, + menu: { + id: MenuId.ExtensionContext, + group: '3_recommendations', + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')), + order: 2 + }, + run: (accessor: ServicesAccessor, id: string) => accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); @@ -1067,39 +1157,25 @@ class ExtensionsContributions implements IWorkbenchContribution { } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - - async run(accessor: ServicesAccessor): Promise { - return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceRecommendations'); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceRecommendations') }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService); @@ -1115,63 +1191,65 @@ class ExtensionsContributions implements IWorkbenchContribution { } }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', - title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations'); - } + this.registerExtensionAction({ + id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', + title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)), + }, + run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations') }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: ConfigureWorkspaceRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.isEqualTo('workspace'), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run(); - } + this.registerExtensionAction({ + id: ConfigureWorkspaceRecommendedExtensionsAction.ID, + title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, + category: localize('extensions', "Extensions"), + menu: { + id: MenuId.CommandPalette, + when: WorkbenchStateContext.isEqualTo('workspace'), + }, + run: () => runAction(this.instantiationService.createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL)) }); - registerAction2(class extends Action2 { - - constructor() { - super({ - id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, - title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' }, - category: localize('extensions', "Extensions"), - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.notEqualsTo('empty'), - }, - }); - } - - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run(); - } - }); } + + private registerExtensionAction(extensionActionOptions: IExtensionActionOptions): IDisposable { + const menus = extensionActionOptions.menu ? isArray(extensionActionOptions.menu) ? extensionActionOptions.menu : [extensionActionOptions.menu] : []; + let menusWithOutTitles: ({ id: MenuId } & Omit)[] = []; + const menusWithTitles: { id: MenuId, item: IMenuItem }[] = []; + if (extensionActionOptions.menuTitles) { + for (let index = 0; index < menus.length; index++) { + const menu = menus[index]; + const menuTitle = extensionActionOptions.menuTitles[menu.id.id]; + if (menuTitle) { + menusWithTitles.push({ id: menu.id, item: { ...menu, command: { id: extensionActionOptions.id, title: menuTitle } } }); + } else { + menusWithOutTitles.push(menu); + } + } + } else { + menusWithOutTitles = menus; + } + const disposables = new DisposableStore(); + disposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + ...extensionActionOptions, + menu: menusWithOutTitles + }); + } + run(accessor: ServicesAccessor, ...args: any[]): Promise { + return extensionActionOptions.run(accessor, ...args); + } + })); + if (menusWithTitles.length) { + disposables.add(MenuRegistry.appendMenuItems(menusWithTitles)); + } + return disposables; + } + } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -1182,7 +1260,6 @@ workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInstaller, LifecyclePhase.Eventually); // Running Extensions const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index ac574d7f2b..cb0216d2bb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -6,23 +6,21 @@ import 'vs/css!./media/extensionActions'; import { localize } from 'vs/nls'; import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; -import { Delayer } from 'vs/base/common/async'; +import { Delayer, Promises } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { dispose } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IFileService, IFileContent } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -40,22 +38,18 @@ import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IQuickPickItem, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IViewsService } from 'vs/workbench/common/views'; import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; @@ -64,46 +58,10 @@ import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOpti import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { ILogService } from 'vs/platform/log/common/log'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { clearSearchResultsIcon, infoIcon, manageExtensionIcon, refreshIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; // {{SQL CARBON EDIT}} import product from 'vs/platform/product/common/product'; - -const promptDownloadManually = (extension: IGalleryExtension | undefined, message: string, error: Error, instantiationService: IInstantiationService): Promise => { - return instantiationService.invokeFunction(accessor => { - if (isPromiseCanceledError(error)) { - return Promise.resolve(); - } - - const productService = accessor.get(IProductService); - const openerService = accessor.get(IOpenerService); - const notificationService = accessor.get(INotificationService); - const dialogService = accessor.get(IDialogService); - const erorrsToShows = [INSTALL_ERROR_INCOMPATIBLE, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_NOT_SUPPORTED]; - if (!extension || erorrsToShows.indexOf(error.name) !== -1 || !productService.extensionsGallery) { - return dialogService.show(Severity.Error, error.message, []); - } else { - const downloadUrl = (extension.assets.downloadPage && extension.assets.downloadPage.uri) || extension.assets.download.uri; // {{SQL CARBON EDIT}} Use the URI directly since we don't have a marketplace hosting the packages - notificationService.prompt(Severity.Error, message, !InstallVSIXAction.AVAILABLE ? [] : [{ - label: localize('download2', "Try Downloading Manually..."), - run: () => openerService.open(URI.parse(downloadUrl)).then(() => { - notificationService.prompt( - Severity.Info, - localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', extension.identifier.id), - [{ - label: InstallVSIXAction.LABEL, - run: () => { - const action = instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); - action.run(); - action.dispose(); - } - }] - ); - }) - }]); - return Promise.resolve(); - } - }); -}; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -139,17 +97,18 @@ function getRelativeDateLabel(date: Date): string { return ''; } -class PromptExtensionInstallFailureAction extends Action { +export class PromptExtensionInstallFailureAction extends Action { constructor( private readonly extension: IExtension, + private readonly version: string, private readonly installOperation: InstallOperation, private readonly error: Error, @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, @ILogService private readonly logService: ILogService, ) { super('extension.promptExtensionInstallFailure'); @@ -165,7 +124,7 @@ class PromptExtensionInstallFailureAction extends Action { : localize('install operation', "Error while installing '{0}' extension.", this.extension.displayName || this.extension.identifier.id); if ([INSTALL_ERROR_INCOMPATIBLE, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_NOT_SUPPORTED].includes(this.error.name)) { - await this.dialogService.show(Severity.Error, `${operationMessage}\n${getErrorMessage(this.error)}`, []); + await this.dialogService.show(Severity.Info, getErrorMessage(this.error), []); return; } @@ -173,23 +132,19 @@ class PromptExtensionInstallFailureAction extends Action { if (this.extension.gallery && this.productService.extensionsGallery) { promptChoices.push({ label: localize('download', "Try Downloading Manually..."), - run: () => this.openerService.open(URI.parse(`${this.productService.extensionsGallery!.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.extension.version}/vspackage`)).then(() => { + run: () => this.openerService.open(URI.parse(`${this.productService.extensionsGallery!.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.version}/vspackage`)).then(() => { this.notificationService.prompt( Severity.Info, localize('install vsix', 'Once downloaded, please manually install the downloaded VSIX of \'{0}\'.', this.extension.identifier.id), [{ - label: InstallVSIXAction.LABEL, - run: () => { - const action = this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL); - action.run(); - action.dispose(); - } + label: localize('installVSIX', "Install from VSIX..."), + run: () => this.commandService.executeCommand(SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID) }] ); }) }); } - const checkLogsMessage = localize('check logs', "Please check [logs]({0}) for more details.", `command:${Constants.showWindowLogActionId}`); + const checkLogsMessage = localize('check logs', "Please check the [log]({0}) for more details.", `command:${Constants.showWindowLogActionId}`); this.notificationService.prompt(Severity.Error, `${operationMessage} ${checkLogsMessage}`, promptChoices); } } @@ -342,7 +297,7 @@ export abstract class AbstractInstallAction extends ExtensionAction { console.error(error); - await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Install, error).run(); + await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, error).run(); return undefined; } } @@ -536,7 +491,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { } } - private canInstall(): boolean { + protected canInstall(): boolean { // Disable if extension is not installed or not an user extension if ( !this.extension @@ -544,7 +499,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { || !this.extension.local || this.extension.state !== ExtensionState.Installed || this.extension.type !== ExtensionType.User - || this.extension.enablementState === EnablementState.DisabledByEnvironemt + || this.extension.enablementState === EnablementState.DisabledByEnvironment ) { return false; } @@ -563,6 +518,11 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { return true; } + // Prefers to run on Web + if (this.server === this.extensionManagementServerService.webExtensionManagementServer && prefersExecuteOnWeb(this.extension.local.manifest, this.productService, this.configurationService)) { + return true; + } + if (this.canInstallAnyWhere) { // Can run on UI if (this.server === this.extensionManagementServerService.localExtensionManagementServer && canExecuteOnUI(this.extension.local.manifest, this.productService, this.configurationService)) { @@ -634,6 +594,31 @@ export class LocalInstallAction extends InstallInOtherServerAction { } +export class WebInstallAction extends InstallInOtherServerAction { + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService, + ) { + super(`extensions.webInstall`, extensionManagementServerService.webExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService); + } + + protected getInstallLabel(): string { + return localize('install browser', "Install in Browser"); + } + + protected canInstall(): boolean { + if (super.canInstall()) { + return !!this.extension?.gallery && this.webExtensionsScannerService.canAddExtension(this.extension.gallery); + } + return false; + } + +} + export class UninstallAction extends ExtensionAction { static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); @@ -756,7 +741,7 @@ export class UpdateAction extends ExtensionAction { console.error(err); - return promptDownloadManually(extension.gallery, localize('failedToUpdate', "Failed to update \'{0}\'.", extension.identifier.id), err, this.instantiationService); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err).run(); } } @@ -816,7 +801,8 @@ export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdow updateClass(): void { super.updateClass(); - if (this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + if (this.element && this.dropdownMenuActionViewItem && this.dropdownMenuActionViewItem.element) { + this.element.classList.toggle('empty', (this._action).menuActions.length === 0); this.dropdownMenuActionViewItem.element.classList.toggle('hide', (this._action).menuActions.length === 0); } } @@ -1104,7 +1090,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { await this.extensionsWorkbenchService.installVersion(this.extension!, pick.id); } } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension!, pick.latest ? this.extension!.latestVersion : pick.id, InstallOperation.Install, error).run(); } } return null; @@ -1287,140 +1273,6 @@ export class DisableDropDownAction extends ActionWithDropDownAction { } -export class CheckForUpdatesAction extends Action { - - static readonly ID = 'workbench.extensions.action.checkForUpdates'; - static readonly LABEL = localize('checkForUpdates', "Check for Extension Updates"); - - constructor( - id = CheckForUpdatesAction.ID, - label = CheckForUpdatesAction.LABEL, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IViewletService private readonly viewletService: IViewletService, - @INotificationService private readonly notificationService: INotificationService - ) { - super(id, label, '', true); - } - - private checkUpdatesAndNotify(): void { - const outdated = this.extensionsWorkbenchService.outdated; - if (!outdated.length) { - this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); - return; - } - - let msgAvailableExtensions = outdated.length === 1 ? localize('singleUpdateAvailable', "An extension update is available.") : localize('updatesAvailable', "{0} extension updates are available.", outdated.length); - - const disabledExtensionsCount = outdated.filter(ext => ext.local && !this.extensionEnablementService.isEnabled(ext.local)).length; - if (disabledExtensionsCount) { - if (outdated.length === 1) { - msgAvailableExtensions = localize('singleDisabledUpdateAvailable', "An update to an extension which is disabled is available."); - } else if (disabledExtensionsCount === 1) { - msgAvailableExtensions = localize('updatesAvailableOneDisabled', "{0} extension updates are available. One of them is for a disabled extension.", outdated.length); - } else if (disabledExtensionsCount === outdated.length) { - msgAvailableExtensions = localize('updatesAvailableAllDisabled', "{0} extension updates are available. All of them are for disabled extensions.", outdated.length); - } else { - msgAvailableExtensions = localize('updatesAvailableIncludingDisabled', "{0} extension updates are available. {1} of them are for disabled extensions.", outdated.length, disabledExtensionsCount); - } - } - - this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search('')); - - this.notificationService.info(msgAvailableExtensions); - } - - run(): Promise { - return this.extensionsWorkbenchService.checkForUpdates().then(() => this.checkUpdatesAndNotify()); - } -} - -export class ToggleAutoUpdateAction extends Action { - - constructor( - id: string, - label: string, - private autoUpdateValue: boolean, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(id, label, '', true); - this.updateEnablement(); - configurationService.onDidChangeConfiguration(() => this.updateEnablement()); - } - - private updateEnablement(): void { - this.enabled = this.configurationService.getValue(AutoUpdateConfigurationKey) !== this.autoUpdateValue; - } - - run(): Promise { - return this.configurationService.updateValue(AutoUpdateConfigurationKey, this.autoUpdateValue); - } -} - -export class EnableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.enableAutoUpdate'; - static readonly LABEL = localize('enableAutoUpdate', "Enable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, true, configurationService); - } -} - -export class DisableAutoUpdateAction extends ToggleAutoUpdateAction { - - static readonly ID = 'workbench.extensions.action.disableAutoUpdate'; - static readonly LABEL = localize('disableAutoUpdate', "Disable Auto Updating Extensions"); - - constructor( - id = EnableAutoUpdateAction.ID, - label = EnableAutoUpdateAction.LABEL, - @IConfigurationService configurationService: IConfigurationService - ) { - super(id, label, false, configurationService); - } -} - -export class UpdateAllAction extends Action { - - static readonly ID = 'workbench.extensions.action.updateAllExtensions'; - static readonly LABEL = localize('updateAll', "Update All Extensions"); - - constructor( - id: string, label: string, isPrimary: boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(id, label, '', false); - - if (isPrimary) { - this._register(this.extensionsWorkbenchService.onChange(() => this._onDidChange.fire({ enabled: this.enabled }))); - } - } - - get enabled(): boolean { - return this.extensionsWorkbenchService.outdated.length > 0; - } - - run(): Promise { - return Promise.all(this.extensionsWorkbenchService.outdated.map(e => this.install(e))); - } - - private async install(extension: IExtension): Promise { - try { - await this.extensionsWorkbenchService.install(extension); - } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Update, err).run(); - } - } -} - export class ReloadAction extends ExtensionAction { private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} reload`; @@ -1470,11 +1322,12 @@ export class ReloadAction extends ExtensionAction { } const isUninstalled = this.extension.state === ExtensionState.Uninstalled; - const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; - const isSameExtensionRunning = runningExtension && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); + const runningExtension = this._runningExtensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); if (isUninstalled) { - if (isSameExtensionRunning && !this.extensionService.canRemoveExtension(runningExtension)) { + const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); + const isSameExtensionRunning = runningExtension && (!this.extension.server || this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); + if (!canRemoveRunningExtension && isSameExtensionRunning) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio @@ -1484,6 +1337,7 @@ export class ReloadAction extends ExtensionAction { return; } if (this.extension.local) { + const isSameExtensionRunning = runningExtension && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local); // Extension is running @@ -1763,49 +1617,6 @@ export class SetProductIconThemeAction extends ExtensionAction { } } -export class OpenExtensionsViewletAction extends ShowViewletAction { - - static ID = VIEWLET_ID; - static LABEL = localize('toggleExtensionsViewlet', "Show Extensions"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); - } -} - -export class InstallExtensionsAction extends OpenExtensionsViewletAction { - static ID = 'workbench.extensions.action.installExtensions'; - static LABEL = localize('installExtensions', "Install Extensions"); -} - -export class ShowEnabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showEnabledExtensions'; - static readonly LABEL = localize('showEnabledExtensions', "Show Enabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@enabled '); - viewlet.focus(); - }); - } -} export class ShowInstalledExtensionsAction extends Action { @@ -1829,230 +1640,6 @@ export class ShowInstalledExtensionsAction extends Action { }); } } - -export class ShowDisabledExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showDisabledExtensions'; - static readonly LABEL = localize('showDisabledExtensions', "Show Disabled Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, 'null', true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@disabled '); - viewlet.focus(); - }); - } -} - -export class ClearExtensionsSearchResultsAction extends Action { - - static readonly ID = 'workbench.extensions.action.clearExtensionsSearchResults'; - static readonly LABEL = localize('clearExtensionsSearchResults', "Clear Extensions Search Results"); - - constructor( - id: string, - label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, ThemeIcon.asClassName(clearSearchResultsIcon), true); - } - - async run(): Promise { - const viewPaneContainer = this.viewsService.getActiveViewPaneContainerWithId(VIEWLET_ID); - if (viewPaneContainer) { - const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; - extensionsViewPaneContainer.search(''); - extensionsViewPaneContainer.focus(); - } - } -} - -export class ClearExtensionsInputAction extends ClearExtensionsSearchResultsAction { - - constructor( - id: string, - label: string, - onSearchChange: Event, - getValue: () => string, - @IViewsService viewsService: IViewsService - ) { - super(id, label, viewsService); - this.onSearchChange(getValue()); - this._register(onSearchChange(this.onSearchChange, this)); - } - - private onSearchChange(value: string): void { - this.enabled = !!value; - } -} - -export class RefreshExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.refreshExtension'; - static readonly LABEL = localize('refreshExtension', "Refresh"); - - constructor( - id: string, - label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label, ThemeIcon.asClassName(refreshIcon), true); - } - - async run(): Promise { - const viewPaneContainer = this.viewsService.getActiveViewPaneContainerWithId(VIEWLET_ID); - if (viewPaneContainer) { - const extensionsViewPaneContainer = viewPaneContainer as IExtensionsViewPaneContainer; - return extensionsViewPaneContainer.refresh(); - } - } -} - -export class ShowBuiltInExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listBuiltInExtensions'; - static readonly LABEL = localize('showBuiltInExtensions', "Show Built-in Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@builtin '); - viewlet.focus(); - }); - } -} - -export class ShowOutdatedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.listOutdatedExtensions'; - static readonly LABEL = localize('showOutdatedExtensions', "Show Outdated Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@outdated '); - viewlet.focus(); - }); - } -} - -export class ShowPopularExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showPopularExtensions'; - static readonly LABEL = localize('showPopularExtensions', "Show Popular Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@popular '); - viewlet.focus(); - }); - } -} - -export class PredefinedExtensionFilterAction extends Action { - - constructor( - id: string, - label: string, - private readonly filter: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`${this.filter} `); - viewlet.focus(); - }); - } -} - -export class RecentlyPublishedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.recentlyPublishedExtensions'; - static readonly LABEL = localize('recentlyPublishedExtensions', "Recently Published Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@sort:publishedDate '); - viewlet.focus(); - }); - } -} - -export class ShowRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedExtensions'; - static readonly LABEL = localize('showRecommendedExtensions', "Show Recommended Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - }); - } -} - export class ShowRecommendedExtensionAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtension'; @@ -2116,7 +1703,7 @@ export class InstallRecommendedExtensionAction extends Action { try { await this.extensionWorkbenchService.install(extension); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, InstallOperation.Install, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, err).run(); } } } @@ -2168,68 +1755,6 @@ export class UndoIgnoreExtensionRecommendationAction extends Action { } } -export class ShowRecommendedKeymapExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; - static readonly LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended:keymaps '); - viewlet.focus(); - }); - } -} - -export class ShowLanguageExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showLanguageExtensions'; - static readonly LABEL = localize('showLanguageExtensionsShort', "Language Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@category:"programming languages" @sort:installs '); - viewlet.focus(); - }); - } -} - -export class SearchCategoryAction extends Action { - - constructor( - id: string, - label: string, - private readonly category: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run(); - } -} - export class SearchExtensionsAction extends Action { constructor( @@ -2246,46 +1771,6 @@ export class SearchExtensionsAction extends Action { } } -export class ChangeSortAction extends Action { - - private query: Query; - - constructor( - id: string, - label: string, - onSearchChange: Event, - private sortBy: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - - if (sortBy === undefined) { - throw new Error('bad arguments'); - } - - this.query = Query.parse(''); - this.enabled = false; - this.checked = false; - this._register(onSearchChange(this.onSearchChange, this)); - } - - private onSearchChange(value: string): void { - const query = Query.parse(value); - this.query = new Query(query.value, this.sortBy || query.sortBy, query.groupBy); - this.enabled = !!value && this.query.isValid(); - this.checked = this.enabled && this.query.equals(query); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(this.query.toString()); - viewlet.focus(); - }); - } -} - export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { constructor( @@ -2424,12 +1909,6 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac @ICommandService private readonly commandService: ICommandService ) { super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService); - this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this)); - this.update(); - } - - private update(): void { - this.enabled = this.contextService.getWorkspace().folders.length > 0; } public run(): Promise { @@ -3042,7 +2521,7 @@ export class ReinstallAction extends Action { } private reinstallExtension(extension: IExtension): Promise { - return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run() + return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() .then(() => { return this.extensionsWorkbenchService.reinstall(extension) .then(extension => { @@ -3129,7 +2608,7 @@ export class InstallSpecificVersionOfExtensionAction extends Action { } private install(extension: IExtension, version: string): Promise { - return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run() + return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() .then(() => { return this.extensionsWorkbenchService.installVersion(extension, version) .then(extension => { @@ -3279,7 +2758,7 @@ export class InstallLocalExtensionsInRemoteAction extends AbstractInstallExtensi protected async installExtensions(localExtensionsToInstall: IExtension[]): Promise { const galleryExtensions: IGalleryExtension[] = []; const vsixs: URI[] = []; - await Promise.all(localExtensionsToInstall.map(async extension => { + await Promises.settled(localExtensionsToInstall.map(async extension => { if (this.extensionGalleryService.isEnabled()) { const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, extension.version); if (gallery) { @@ -3291,8 +2770,8 @@ export class InstallLocalExtensionsInRemoteAction extends AbstractInstallExtensi vsixs.push(vsix); })); - await Promise.all(galleryExtensions.map(gallery => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(gallery))); - await Promise.all(vsixs.map(vsix => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.install(vsix))); + await Promises.settled(galleryExtensions.map(gallery => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(gallery))); + await Promises.settled(vsixs.map(vsix => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.install(vsix))); } } @@ -3327,7 +2806,7 @@ export class InstallRemoteExtensionsInLocalAction extends AbstractInstallExtensi protected async installExtensions(extensions: IExtension[]): Promise { const galleryExtensions: IGalleryExtension[] = []; const vsixs: URI[] = []; - await Promise.all(extensions.map(async extension => { + await Promises.settled(extensions.map(async extension => { if (this.extensionGalleryService.isEnabled()) { const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, extension.version); if (gallery) { @@ -3339,8 +2818,8 @@ export class InstallRemoteExtensionsInLocalAction extends AbstractInstallExtensi vsixs.push(vsix); })); - await Promise.all(galleryExtensions.map(gallery => this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.installFromGallery(gallery))); - await Promise.all(vsixs.map(vsix => this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.install(vsix))); + await Promises.settled(galleryExtensions.map(gallery => this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.installFromGallery(gallery))); + await Promises.settled(vsixs.map(vsix => this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.install(vsix))); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts b/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts index 75a8e15931..4d322dc51c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts @@ -15,6 +15,7 @@ import { Action } from 'vs/base/common/actions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { Disposable } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Promises } from 'vs/base/common/async'; export class ExtensionDependencyChecker extends Disposable implements IWorkbenchContribution { @@ -62,7 +63,7 @@ export class ExtensionDependencyChecker extends Disposable implements IWorkbench if (missingDependencies.length) { const extensions = (await this.extensionsWorkbenchService.queryGallery({ names: missingDependencies, pageSize: missingDependencies.length }, CancellationToken.None)).firstPage; if (extensions.length) { - await Promise.all(extensions.map(extension => this.extensionsWorkbenchService.install(extension))); + await Promises.settled(extensions.map(extension => this.extensionsWorkbenchService.install(extension))); this.notificationService.notify({ severity: Severity.Info, message: localize('finished installing missing deps', "Finished installing missing dependencies. Please reload the window now."), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index e109ce6991..ba5bb73f2f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -14,7 +14,7 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Label, RatingsWidget, /*InstallCountWidget,*/ RecommendationWidget, RemoteBadgeWidget, TooltipWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -113,6 +113,7 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), + this.instantiationService.createInstance(WebInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, false), systemDisabledWarningAction, this.instantiationService.createInstance(ManageExtensionAction) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index be207a500d..5d16d013ed 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -5,27 +5,21 @@ import 'vs/css!./media/extensionsViewlet'; import { localize } from 'vs/nls'; -import { timeout, Delayer } from 'vs/base/common/async'; +import { timeout, Delayer, Promises } from 'vs/base/common/async'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAction, Action, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; +import { Action } from 'vs/base/common/actions'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { append, $, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from '../common/extensions'; -import { - ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, SearchCategoryAction, - /*RecentlyPublishedExtensionsAction, */ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ShowDisabledExtensionsAction, - ShowEnabledExtensionsAction, PredefinedExtensionFilterAction, RefreshExtensionsAction -} from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { OpenExtensionAuthoringDocsAction } from 'sql/workbench/contrib/extensions/browser/extensionsActions'; // {{SQL CARBON EDIT}} -import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID } from '../common/extensions'; +import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; @@ -33,24 +27,25 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { ExtensionType, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; @@ -63,10 +58,9 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { isWeb } from 'vs/base/common/platform'; -import { memoize } from 'vs/base/common/decorators'; -import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); const SearchOutdatedExtensionsContext = new RawContextKey('searchOutdatedExtensions', false); @@ -170,7 +164,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio get name() { return getInstalledViewName(); }, weight: 100, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('hasInstalledExtensions')), /* Empty installed extensions view shall have fixed height */ ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, fixedHeight: true, onDidChangeTitle }]), /* Empty installed extensions views shall not be allowed to hidden */ @@ -183,11 +177,49 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio get name() { return getInstalledViewName(); }, weight: 100, order: 1, - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, isWebServer ? ContextKeyExpr.has('hasInstalledWebExtensions') : ContextKeyExpr.has('hasInstalledExtensions')), ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, onDidChangeTitle }]), /* Installed extensions views shall not be allowed to hidden when there are more than one server */ canToggleVisibility: servers.length === 1 }); + + if (server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer) { + registerAction2(class InstallLocalExtensionsInRemoteAction2 extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.installLocalExtensions', + get title() { return localize('select and install local extensions', "Install Local Extensions in '{0}'...", server.label); }, + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), + icon: installLocalInRemoteIcon, + f1: true, + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', id), + group: 'navigation', + } + }); + } + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(InstallLocalExtensionsInRemoteAction).run(); + } + }); + } + } + + if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { + registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.actions.installLocalExtensionsInRemote', + title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' }, + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), + f1: true + }); + } + run(accessor: ServicesAccessor): Promise { + return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); + } + }); } /* @@ -215,7 +247,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'extensions.recommendedList', name: localize('recommendedExtensions', "Marketplace"), // {{SQL CARBON EDIT}} - change name to marketplace ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand')), weight: 40, order: 3, canToggleVisibility: true @@ -231,7 +263,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.enabled', name: localize('enabledExtensions', "Enabled"), ctorDescriptor: new SyncDescriptor(EnabledExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')), hideByDefault: true, weight: 40, order: 4, @@ -246,7 +278,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.disabled', name: localize('disabledExtensions', "Disabled"), ctorDescriptor: new SyncDescriptor(DisabledExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions')), + when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')), hideByDefault: true, weight: 10, order: 5, @@ -328,7 +360,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio const viewDescriptors: IViewDescriptor[] = []; viewDescriptors.push({ - id: 'workbench.views.extensions.workspaceRecommendations', + id: WORKSPACE_RECOMMENDATIONS_VIEW_ID, name: localize('workspaceRecommendedExtensions', "Workspace Recommendations"), ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView, [{}]), when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), WorkbenchStateContext.notEqualsTo('empty')), @@ -377,9 +409,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer { - private readonly _onSearchChange: Emitter = this._register(new Emitter()); - private readonly onSearchChange: Event = this._onSearchChange.event; private defaultViewsContextKey: IContextKey; + private sortByContextKey: IContextKey; private searchMarketplaceExtensionsContextKey: IContextKey; private searchInstalledExtensionsContextKey: IContextKey; private searchOutdatedExtensionsContextKey: IContextKey; @@ -395,8 +426,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private root: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; private readonly searchViewletState: MementoObject; - private readonly sortActions: ChangeSortAction[]; - private secondaryActions: IAction[] | undefined = undefined; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -406,7 +435,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -424,6 +452,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchDelayer = new Delayer(500); this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService); + this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); @@ -437,13 +466,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this)); this.searchViewletState = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - this.secondaryActions = undefined; - this.updateTitleArea(); - } - }, this)); - if (extensionManagementServerService.webExtensionManagementServer) { this._register(extensionsWorkbenchService.onChange(() => { // show installed web extensions view only when it is not visible @@ -453,13 +475,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } })); } + } - this.sortActions = [ - // this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Install Count"), this.onSearchChange, 'installs')), // {{SQL CARBON EDIT}} - // this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Rating"), this.onSearchChange, 'rating')), // {{SQL CARBON EDIT}} - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.name', localize('sort by name', "Name"), this.onSearchChange, 'name')), - this._register(this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.publishedDate', localize('sort by date', "Published Date"), this.onSearchChange, 'publishedDate')), - ]; + get searchValue(): string | undefined { + return this.searchBox?.getValue(); } create(parent: HTMLElement): void { @@ -494,8 +513,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(attachSuggestEnabledInputBoxStyler(this.searchBox, this.themeService)); this._register(this.searchBox.onInputDidChange(() => { + this.sortByContextKey.set(Query.parse(this.searchBox!.getValue() || '').sortBy); this.triggerSearch(); - this._onSearchChange.fire(this.searchBox!.getValue()); }, this)); this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); @@ -572,61 +591,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE return 400; } - @memoize - getActions(): IAction[] { - // Local extensions filters - let filterActions: IAction[] = [ - this._register(this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, localize('builtin filter', "Built-in"))), - this._register(this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, localize('installed filter', "Installed"))), - this._register(this.instantiationService.createInstance(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, localize('enabled filter', "Enabled"))), - this._register(this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, localize('disabled filter', "Disabled"))), - this._register(this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, localize('outdated filter', "Outdated"))), - ]; - - if (this.extensionGalleryService.isEnabled()) { - filterActions = [ - // this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured'), // {{SQL CARBON EDIT}} - // this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular'), // {{SQL CARBON EDIT}} - this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended'), - // this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published")), // {{SQL CARBON EDIT}} - new Separator(), - new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))), - new Separator(), - ...filterActions, - this._register(new Separator()), - this._register(new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions)), - ]; - } - - return [ - this._register(new SubmenuAction('workbench.extensions.action.filterExtensions', localize('filterExtensions', "Filter Extensions..."), filterActions, ThemeIcon.asClassName(filterIcon))), - this._register(this.instantiationService.createInstance(RefreshExtensionsAction, RefreshExtensionsAction.ID, RefreshExtensionsAction.LABEL)), - this._register(this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, () => this.searchBox!.getValue() || '')), - ]; - } - - getSecondaryActions(): IAction[] { - if (!this.secondaryActions) { - this.secondaryActions = []; - this.secondaryActions.push(this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL)); - if (this.configurationService.getValue(AutoUpdateConfigurationKey)) { - this.secondaryActions.push(this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)); - } else { - this.secondaryActions.push(this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL, false), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)); - } - - this.secondaryActions.push(new Separator()); - this.secondaryActions.push(this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL, false)); - this.secondaryActions.push(this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL, false)); - - this.secondaryActions.push(new Separator()); - this.secondaryActions.push(this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL)); - this.secondaryActions.push(this.instantiationService.createInstance(OpenExtensionAuthoringDocsAction, OpenExtensionAuthoringDocsAction.ID, OpenExtensionAuthoringDocsAction.LABEL)); // {{SQL CARBON EDIT}} - - } - return this.secondaryActions; - } - search(value: string): void { if (this.searchBox && this.searchBox.getValue() !== value) { this.searchBox.setValue(value); @@ -839,7 +803,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { .filter(e => maliciousSet.has(e.identifier.id)); if (maliciousExtensions.length) { - return Promise.all(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e).then(() => { + return Promises.settled(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e).then(() => { this.notificationService.prompt( Severity.Warning, localize('malicious warning', "We have uninstalled '{0}' which was reported to be problematic.", e.identifier.id), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 7d5ef731f5..72457f34d1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -17,19 +17,19 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { append, $ } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState, EXTENSION_LIST_ELEMENT_HEIGHT } from 'vs/workbench/contrib/extensions/browser/extensionsList'; -import { ExtensionState, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionState, IExtension, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/common/extensions'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; // {{SQL CARBON EDIT}} import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; @@ -49,7 +49,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { configureRecommendedIcon, installLocalInRemoteIcon, installWorkspaceRecommendedIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -134,8 +133,12 @@ export class ExtensionsListView extends ViewPane { if (this.options.onDidChangeTitle) { this._register(this.options.onDidChangeTitle(title => this.updateTitle(title))); } + + this.registerActions(); } + protected registerActions(): void { } + protected renderHeader(container: HTMLElement): void { container.classList.add('extension-view-header'); super.renderHeader(container); @@ -177,7 +180,7 @@ export class ExtensionsListView extends ViewPane { const resourceNavigator = this._register(new ListResourceNavigator(this.list, { openOnSingleClick: true })); this._register(Event.debounce(Event.filter(resourceNavigator.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { - this.openExtension(this.list!.model.get(options.element!), { sideByside: options.sideBySide, ...options.editorOptions }); + this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); })); this.bodyTemplate = { @@ -263,32 +266,27 @@ export class ExtensionsListView extends ViewPane { const runningExtensions = await this.extensionService.getExtensions(); const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); manageExtensionAction.extension = e.element; + let groups: IAction[][] = []; if (manageExtensionAction.enabled) { - const groups = await manageExtensionAction.getActionGroups(runningExtensions); - let actions: IAction[] = []; - for (const menuActions of groups) { - actions = [...actions, ...menuActions, new Separator()]; - } - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions.slice(0, actions.length - 1) - }); + groups = await manageExtensionAction.getActionGroups(runningExtensions); + } else if (e.element) { - const groups = getContextMenuActions(e.element, false, this.instantiationService); + groups = getContextMenuActions(e.element, false, this.instantiationService); groups.forEach(group => group.forEach(extensionAction => { if (extensionAction instanceof ExtensionAction) { extensionAction.extension = e.element!; } })); - let actions: IAction[] = []; - for (const menuActions of groups) { - actions = [...actions, ...menuActions, new Separator()]; - } - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions - }); } + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } + actions.pop(); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions + }); } } @@ -775,8 +773,14 @@ export class ExtensionsListView extends ViewPane { } // All recommendations - if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { - return this.getAllRecommendationsModel(query, options, token); + if (/@recommended:all/i.test(query.value)) { + return this.getAllRecommendationsModel(options, token); + } + + // Search recommendations + if (ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) || + (ExtensionsListView.isRecommendedExtensionsQuery(query.value) && options.sortBy !== undefined)) { + return this.searchRecommendations(query, options, token); } // Other recommendations @@ -812,10 +816,8 @@ export class ExtensionsListView extends ViewPane { } private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); const recommendations = await this.getWorkspaceRecommendations(); - const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)) - .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)); this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); return new PagedModel(result); @@ -837,14 +839,19 @@ export class ExtensionsListView extends ViewPane { } private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + const otherRecommendations = await this.getOtherRecommendations(); + const installableRecommendations = await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token); + const result = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); + } + private async getOtherRecommendations(): Promise { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)) .map(e => e.identifier.id.toLowerCase()); const workspaceRecommendations = (await this.getWorkspaceRecommendations()) .map(extensionId => extensionId.toLowerCase()); - const otherRecommendations = distinct( + return distinct( flatten(await Promise.all([ // Order is important this.extensionRecommendationsService.getImportantRecommendations(), @@ -852,16 +859,10 @@ export class ExtensionsListView extends ViewPane { this.extensionRecommendationsService.getOtherRecommendations() ])).filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase()) ), extensionId => extensionId.toLowerCase()); - - const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token)) - .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - - const result: IExtension[] = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); - return new PagedModel(result); } // Get All types of recommendations, trimmed to show a max of 8 at any given time - private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + private async getAllRecommendationsModel(options: IQueryOptions, token: CancellationToken): Promise> { const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server)).map(e => e.identifier.id.toLowerCase()); const allRecommendations = distinct( @@ -879,6 +880,15 @@ export class ExtensionsListView extends ViewPane { return new PagedModel(result.slice(0, 8)); } + private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + const result = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(this.sortExtensions(result, options)); + } + // Sorts the firstPage of the pager in the same order as given array of extension ids private sortFirstPage(pager: IPager, ids: string[]) { ids = ids.map(x => x.toLowerCase()); @@ -1033,7 +1043,7 @@ export class ExtensionsListView extends ViewPane { } static isSearchRecommendedExtensionsQuery(query: string): boolean { - return /@recommended/i.test(query) && !ExtensionsListView.isRecommendedExtensionsQuery(query); + return /@recommended\s.+/i.test(query); } static isWorkspaceRecommendedExtensionsQuery(query: string): boolean { @@ -1076,14 +1086,6 @@ export class ServerInstalledExtensionsView extends ExtensionsListView { return super.show(query.trim()); } - getActions(): IAction[] { - if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.options.server) { - const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)); - installLocalExtensionsInRemoteAction.class = ThemeIcon.asClassName(installLocalInRemoteIcon); - return [installLocalExtensionsInRemoteAction]; - } - return []; - } } export class EnabledExtensionsView extends ExtensionsListView { @@ -1163,9 +1165,8 @@ export class RecommendedExtensionsView extends ExtensionsListView { } } -export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { +export class WorkspaceRecommendedExtensionsView extends ExtensionsListView implements IWorkspaceRecommendedExtensionsView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: Action | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); @@ -1174,31 +1175,13 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery))); } - getActions(): IAction[] { - if (!this.installAllAction) { - this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), ThemeIcon.asClassName(installWorkspaceRecommendedIcon), false, () => this.installWorkspaceRecommendations())); - } - - const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); - configureWorkspaceFolderAction.class = ThemeIcon.asClassName(configureRecommendedIcon); - return [this.installAllAction, configureWorkspaceFolderAction]; - } - async show(query: string): Promise> { let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); - await this.setRecommendationsToInstall(); return model; } - private async setRecommendationsToInstall(): Promise { - const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); - if (this.installAllAction) { - this.installAllAction.enabled = installableRecommendations.length > 0; - } - } - private async getInstallableWorkspaceRecommendations() { const installed = (await this.extensionsWorkbenchService.queryLocal()) .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind @@ -1207,9 +1190,16 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } - private async installWorkspaceRecommendations(): Promise { + async installWorkspaceRecommendations(): Promise { const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); - await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + if (installableRecommendations.length) { + await this.extensionManagementService.installExtensions(installableRecommendations.map(i => i.gallery!)); + } else { + this.notificationService.notify({ + severity: Severity.Info, + message: localize('no local extensions', "There are no extensions to install.") + }); + } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index fc5d598dce..fb184b8b74 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; import { Event, Emitter } from 'vs/base/common/event'; import { index, distinct } from 'vs/base/common/arrays'; -import { ThrottledDelayer } from 'vs/base/common/async'; +import { Promises, ThrottledDelayer } from 'vs/base/common/async'; import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; @@ -23,7 +23,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -44,6 +44,7 @@ import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDa import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; // {{SQL CARBON EDIT}} import { IOpenerService } from 'vs/platform/opener/common/opener'; // {{SQL CARBON EDIT}} +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; interface IExtensionStateProvider { (extension: Extension): T; @@ -517,6 +518,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private static readonly SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours declare readonly _serviceBrand: undefined; + private hasOutdatedExtensionsContextKey: IContextKey; + private readonly localExtensions: Extensions | null = null; private readonly remoteExtensions: Extensions | null = null; private readonly webExtensions: Extensions | null = null; @@ -546,10 +549,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @IProductService private readonly productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService private readonly openerService: IOpenerService, // {{SQL CARBON EDIT}} @IExtensionManagementService private readonly extensionService: IExtensionManagementService ) { super(); + this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext))); this._register(this.localExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); @@ -587,7 +592,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.eventuallySyncWithGallery(true); }); - this._register(this.onChange(() => this.updateActivity())); + this._register(this.onChange(() => { + this.updateContexts(); + this.updateActivity(); + })); } get local(): IExtension[] { @@ -889,7 +897,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension promises.push(this.queryGallery({ names, pageSize: names.length }, CancellationToken.None)); } - return Promise.all(promises).then(() => undefined); + return Promises.settled(promises).then(() => undefined); } private eventuallyAutoUpdateExtensions(): void { @@ -1249,13 +1257,21 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return changed; } + private updateContexts(extension?: Extension): void { + if (extension && extension.outdated) { + this.hasOutdatedExtensionsContextKey.set(true); + } else { + this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); + } + } + private _activityCallBack: ((value: void) => void) | null = null; private updateActivity(): void { if ((this.localExtensions && this.localExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) || (this.remoteExtensions && this.remoteExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling)) || (this.webExtensions && this.webExtensions.local.some(e => e.state === ExtensionState.Installing || e.state === ExtensionState.Uninstalling))) { if (!this._activityCallBack) { - this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(c => this._activityCallBack = c)); + this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(resolve => this._activityCallBack = resolve)); } } else { if (this._activityCallBack) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 7776bb73d8..c1637910d1 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -179,3 +179,7 @@ .extension-list-item > .details > .footer > .monaco-action-bar > .actions-container .extension-action.label { max-width: 150px; } + +.extension-list-item .footer .monaco-action-bar .action-item.action-dropdown-item.empty > .action-label { + margin-right: 4px; +} diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts deleted file mode 100644 index 6e01713511..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { localize } from 'vs/nls'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; - -export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchContribution { - - constructor( - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @ILabelService labelService: ILabelService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(); - if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - const installLocalExtensionsInRemoteAction = instantiationService.createInstance(InstallLocalExtensionsInRemoteAction); - CommandsRegistry.registerCommand('workbench.extensions.installLocalExtensions', () => installLocalExtensionsInRemoteAction.run()); - let disposable = Disposable.None; - const appendMenuItem = () => { - disposable.dispose(); - disposable = MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'workbench.extensions.installLocalExtensions', - category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), - title: installLocalExtensionsInRemoteAction.label - } - }); - }; - appendMenuItem(); - this._register(labelService.onDidChangeFormatters(e => appendMenuItem())); - this._register(toDisposable(() => disposable.dispose())); - - this._register(registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 { - constructor() { - super({ - id: 'workbench.extensions.actions.installLocalExtensionsInRemote', - title: { value: localize('install remote in local', "Install Remote Extensions Locally..."), original: 'Install Remote Extensions Locally...' }, - category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), - f1: true - }); - } - run(accessor: ServicesAccessor): Promise { - return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run(); - } - })); - } - } - -} diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 548771769e..83a9b0bb39 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -13,17 +13,23 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { IViewPaneContainer } from 'vs/workbench/common/views'; +import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const VIEWLET_ID = 'workbench.view.extensions'; export const EXTENSIONS_CONFIG = '.azuredatastudio/extensions.json'; export interface IExtensionsViewPaneContainer extends IViewPaneContainer { + readonly searchValue: string | undefined; search(text: string): void; refresh(): Promise; } +export interface IWorkspaceRecommendedExtensionsView extends IView { + installWorkspaceRecommendations(): Promise; +} + export const enum ExtensionState { Installing, Installed, @@ -152,5 +158,12 @@ export class ExtensionContainers extends Disposable { } } +export const WORKSPACE_RECOMMENDATIONS_VIEW_ID = 'workbench.views.extensions.workspaceRecommendations'; export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension'; +export const SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID = 'workbench.extensions.action.installVSIX'; export const INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID = 'workbench.extensions.command.installFromVSIX'; + +// Context Keys +export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); +export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); +export const HasOutdatedExtensionsContext = new RawContextKey('hasOutdatedExtensions', false); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 473940663e..ff7cb37458 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -21,7 +21,6 @@ import { ExtensionHostProfileService } from 'vs/workbench/contrib/extensions/ele import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionsAutoProfiler } from 'vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-sandbox/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; @@ -62,11 +61,10 @@ const actionRegistry = Registry.as(WorkbenchActionExte class ExtensionsContributions implements IWorkbenchContribution { constructor( - @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IExtensionRecommendationNotificationService extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { - sharedProcessService.registerChannel('IExtensionRecommendationNotificationService', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); + sharedProcessService.registerChannel('extensionRecommendationNotification', new ExtensionRecommendationNotificationServiceChannel(extensionRecommendationNotificationService)); const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction); actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts index 2ac301eccb..47e7af95e6 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsAutoProfiler.ts @@ -11,7 +11,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { joinPath } from 'vs/base/common/resources'; -import { writeFile } from 'vs/base/node/pfs'; import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; @@ -22,6 +21,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution { @@ -36,7 +37,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont @INotificationService private readonly _notificationService: INotificationService, @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService + @INativeWorkbenchEnvironmentService private readonly _environmentServie: INativeWorkbenchEnvironmentService, + @IFileService private readonly _fileService: IFileService ) { super(); this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this)); @@ -139,8 +141,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont // print message to log - const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`).fsPath; - await writeFile(path, JSON.stringify(profile.data)); + const path = joinPath(this._environmentServie.tmpDir, `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`); + await this._fileService.writeFile(path, VSBuffer.fromString(JSON.stringify(profile.data))); this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top!.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts b/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts index c4e34b1a09..433b878c2d 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/reportExtensionIssueAction.ts @@ -14,6 +14,8 @@ import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/com import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; +const builtinExtensionIssueUrl = 'https://github.com/microsoft/vscode'; + export class ReportExtensionIssueAction extends Action { private static readonly _id = 'workbench.extensions.action.reportExtensionIssue'; @@ -34,7 +36,8 @@ export class ReportExtensionIssueAction extends Action { @INativeHostService private readonly nativeHostService: INativeHostService ) { super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue'); - this.enabled = !!extension.description.repository && !!extension.description.repository.url; + + this.enabled = extension.description.isBuiltin || (!!extension.description.repository && !!extension.description.repository.url); } async run(): Promise { @@ -51,6 +54,9 @@ export class ReportExtensionIssueAction extends Action { unresponsiveProfile?: IExtensionHostProfile }): Promise { let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined; + if (!baseUrl && extension.description.isBuiltin) { + baseUrl = builtinExtensionIssueUrl; + } if (!!baseUrl) { baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`; } else { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index 7f5c44ea24..b5a80c5ac1 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -5,11 +5,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; -import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; -import * as os from 'os'; import * as uuid from 'vs/base/common/uuid'; -import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, IExtensionTipsService @@ -47,8 +43,6 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; -import { Schemas } from 'vs/base/common/network'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; @@ -62,6 +56,11 @@ import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/e import { ExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { joinPath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -181,7 +180,6 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} let instantiationService: TestInstantiationService; let testConfigurationService: TestConfigurationService; let testObject: ExtensionRecommendationsService; - let parentResource: string; let installEvent: Emitter, didInstallEvent: Emitter, uninstallEvent: Emitter, @@ -203,6 +201,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IExtensionManagementService, >{ onInstallExtension: installEvent.event, onDidInstallExtension: didInstallEvent.event, @@ -278,34 +277,30 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} }); }); - teardown(done => { - (testObject).dispose(); - if (parentResource) { - rimraf(parentResource, RimRafMode.MOVE).then(done, done); - } else { - done(); - } - }); + teardown(() => (testObject).dispose()); function setUpFolderWorkspace(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const id = uuid.generateUuid(); - parentResource = path.join(os.tmpdir(), 'vsctests', id); - return setUpFolder(folderName, parentResource, recommendedExtensions, ignoredRecommendations); + return setUpFolder(folderName, recommendedExtensions, ignoredRecommendations); } - async function setUpFolder(folderName: string, parentDir: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { - const folderDir = path.join(parentDir, folderName); - const workspaceSettingsDir = path.join(folderDir, '.vscode'); - await mkdirp(workspaceSettingsDir, 493); - const configPath = path.join(workspaceSettingsDir, 'extensions.json'); - fs.writeFileSync(configPath, JSON.stringify({ + async function setUpFolder(folderName: string, recommendedExtensions: string[], ignoredRecommendations: string[] = []): Promise { + const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + const logService = new NullLogService(); + const fileService = new FileService(logService); + const fileSystemProvider = new InMemoryFileSystemProvider(); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folderDir = joinPath(ROOT, folderName); + const workspaceSettingsDir = joinPath(folderDir, '.vscode'); + await fileService.createFolder(workspaceSettingsDir); + const configPath = joinPath(workspaceSettingsDir, 'extensions.json'); + await fileService.writeFile(configPath, VSBuffer.fromString(JSON.stringify({ 'recommendations': recommendedExtensions, 'unwantedRecommendations': ignoredRecommendations, - }, null, '\t')); + }, null, '\t'))); + + const myWorkspace = testWorkspace(folderDir); - const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); - const fileService = new FileService(new NullLogService()); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); instantiationService.stub(IFileService, fileService); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 0ff6ca06e0..d03ea9b1a2 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -33,7 +33,7 @@ import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -53,6 +53,8 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -77,6 +79,7 @@ async function setupTest() { instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(IProductService, {}); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); @@ -885,83 +888,6 @@ suite('ExtensionsActions', () => { }); }); - test('Test UpdateAllAction when no installed extensions', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - - assert.ok(!testObject.enabled); - }); - - test('Test UpdateAllAction when installed extensions are not outdated', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [aLocalExtension('a'), aLocalExtension('b')]); - return instantiationService.get(IExtensionsWorkbenchService).queryLocal() - .then(extensions => assert.ok(!testObject.enabled)); - }); - - test('Test UpdateAllAction when some installed extensions are outdated', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(async () => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest))); - assert.ok(!testObject.enabled); - return new Promise(c => { - testObject.onDidChange(() => { - if (testObject.enabled) { - c(); - } - }); - workbenchService.queryGallery(CancellationToken.None); - }); - }); - }); - - test('Test UpdateAllAction when some installed extensions are outdated and some outdated are being installed', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(async () => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); - assert.ok(!testObject.enabled); - return new Promise(c => { - installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); - testObject.onDidChange(() => { - if (testObject.enabled) { - c(); - } - }); - workbenchService.queryGallery(CancellationToken.None); - }); - }); - }); - - test('Test UpdateAllAction when some installed extensions are outdated and all outdated are being installed', () => { - const testObject: ExtensionsActions.UpdateAllAction = instantiationService.createInstance(ExtensionsActions.UpdateAllAction, 'id', 'label', true); - const local = [aLocalExtension('a', { version: '1.0.1' }), aLocalExtension('b', { version: '1.0.1' }), aLocalExtension('c', { version: '1.0.1' })]; - const gallery = [aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest)]; - const workbenchService = instantiationService.get(IExtensionsWorkbenchService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', local); - return workbenchService.queryLocal() - .then(() => { - instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); - return workbenchService.queryGallery(CancellationToken.None) - .then(() => { - installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); - installEvent.fire({ identifier: local[1].identifier, gallery: gallery[1] }); - assert.ok(!testObject.enabled); - }); - }); - }); - - test(`RecommendToFolderAction`, () => { - // TODO: Implement test - }); - }); suite('ReloadAction', () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 12dc08ffd0..fc7717d94a 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -35,7 +35,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { SinonStub } from 'sinon'; import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -165,7 +165,8 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IViewDescriptorService, { getViewLocationById(): ViewContainerLocation { return ViewContainerLocation.Sidebar; - } + }, + onDidChangeLocation: Event.None }); instantiationService.stub(IExtensionService, >{ diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 32d950bf2d..f069cdd140 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -36,7 +36,7 @@ import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType, IExtension, ExtensionKind } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; +import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -46,6 +46,8 @@ import { IExperimentService } from 'vs/workbench/contrib/experiments/common/expe import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { Schemas } from 'vs/base/common/network'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -72,6 +74,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(IURLService, NativeURLService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IConfigurationService, >{ diff --git a/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts b/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts new file mode 100644 index 0000000000..eb371dd26a --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/configuration.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import * as nls from 'vs/nls'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export const defaultExternalUriOpenerId = 'default'; + +export const externalUriOpenersSettingId = 'workbench.externalUriOpeners'; + +export interface ExternalUriOpenersConfiguration { + readonly [uriGlob: string]: string; +} + +const externalUriOpenerIdSchemaAddition: IJSONSchema = { + type: 'string', + enum: [] +}; + +const exampleUriPatterns = ` +- \`https://microsoft.com\`: Matches this specific domain using https +- \`https://microsoft.com:8080\`: Matches this specific domain on this port using https +- \`https://microsoft.com:*\`: Matches this specific domain on any port using https +- \`https://microsoft.com/foo\`: Matches \`https://microsoft.com/foo\` and \`https://microsoft.com/foo/bar\`, but not \`https://microsoft.com/foobar\` or \`https://microsoft.com/bar\` +- \`https://*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using https +- \`microsoft.com\`: Match this specific domain using either http or https +- \`*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using either http or https +- \`http://192.168.0.1\`: Matches this specific IP using http +- \`http://192.168.0.*\`: Matches all IP's with this prefix using http +- \`*\`: Match all domains using either http or https`; + +export const externalUriOpenersConfigurationNode: IConfigurationNode = { + ...workbenchConfigurationNodeBase, + properties: { + [externalUriOpenersSettingId]: { + type: 'object', + markdownDescription: nls.localize('externalUriOpeners', "Configure the opener to use for external uris (i.e. http, https)."), + defaultSnippets: [{ + body: { + 'example.com': '$1' + } + }], + additionalProperties: { + anyOf: [ + { + type: 'string', + markdownDescription: nls.localize('externalUriOpeners.uri', "Map uri pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns), + }, + { + type: 'string', + markdownDescription: nls.localize('externalUriOpeners.uri', "Map uri pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns), + enum: [defaultExternalUriOpenerId], + enumDescriptions: [nls.localize('externalUriOpeners.defaultId', "Open using VS Code's standard opener.")], + }, + externalUriOpenerIdSchemaAddition + ] + } + } + } +}; + +export function updateContributedOpeners(enumValues: string[], enumDescriptions: string[]): void { + externalUriOpenerIdSchemaAddition.enum = enumValues; + externalUriOpenerIdSchemaAddition.enumDescriptions = enumDescriptions; + + Registry.as(Extensions.Configuration) + .notifyConfigurationSchemaUpdated(externalUriOpenersConfigurationNode); +} diff --git a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts new file mode 100644 index 0000000000..8bd79d6452 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; +import { updateContributedOpeners } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +interface RegisteredExternalOpener { + readonly extensionId: string; + + isCurrentlyRegistered: boolean +} + +interface OpenersMemento { + [id: string]: RegisteredExternalOpener; +} + +/** + */ +export class ContributedExternalUriOpenersStore extends Disposable { + + private static readonly STORAGE_ID = 'externalUriOpeners'; + + private readonly _openers = new Map(); + private readonly _memento: Memento; + private _mementoObject: OpenersMemento; + + constructor( + @IStorageService storageService: IStorageService, + @IExtensionService private readonly _extensionService: IExtensionService + ) { + super(); + + this._memento = new Memento(ContributedExternalUriOpenersStore.STORAGE_ID, storageService); + this._mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + for (const id of Object.keys(this._mementoObject || {})) { + this.add(id, this._mementoObject[id].extensionId, { isCurrentlyRegistered: false }); + } + + this.invalidateOpenersOnExtensionsChanged(); + + this._register(this._extensionService.onDidChangeExtensions(() => this.invalidateOpenersOnExtensionsChanged())); + this._register(this._extensionService.onDidChangeExtensionsStatus(() => this.invalidateOpenersOnExtensionsChanged())); + } + + public didRegisterOpener(id: string, extensionId: string): void { + this.add(id, extensionId, { + isCurrentlyRegistered: true + }); + } + + private add(id: string, extensionId: string, options: { isCurrentlyRegistered: boolean }): void { + const existing = this._openers.get(id); + if (existing) { + existing.isCurrentlyRegistered = existing.isCurrentlyRegistered || options.isCurrentlyRegistered; + return; + } + + const entry = { + extensionId, + isCurrentlyRegistered: options.isCurrentlyRegistered + }; + this._openers.set(id, entry); + + this._mementoObject[id] = entry; + this._memento.saveMemento(); + + this.updateSchema(); + } + + public delete(id: string): void { + this._openers.delete(id); + + delete this._mementoObject[id]; + this._memento.saveMemento(); + + this.updateSchema(); + } + + private async invalidateOpenersOnExtensionsChanged() { + const registeredExtensions = await this._extensionService.getExtensions(); + + for (const [id, entry] of this._openers) { + const extension = registeredExtensions.find(r => r.identifier.value === entry.extensionId); + if (extension) { + if (!this._extensionService.canRemoveExtension(extension)) { + // The extension is running. We should have registered openers at this point + if (!entry.isCurrentlyRegistered) { + this.delete(id); + } + } + } else { + // The opener came from an extension that is no longer enabled/installed + this.delete(id); + } + } + } + + private updateSchema() { + const ids: string[] = []; + const descriptions: string[] = []; + + for (const [id, entry] of this._openers) { + ids.push(id); + descriptions.push(entry.extensionId); + } + + updateContributedOpeners(ids, descriptions); + } +} diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts new file mode 100644 index 0000000000..c599dd4333 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpener.contribution.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { externalUriOpenersConfigurationNode } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { ExternalUriOpenerService, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; + +registerSingleton(IExternalUriOpenerService, ExternalUriOpenerService); + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration(externalUriOpenersConfigurationNode); diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts new file mode 100644 index 0000000000..5c318da55e --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts @@ -0,0 +1,241 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { firstOrDefault } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; +import { isWeb } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IExternalOpener, IOpenerService } from 'vs/platform/opener/common/opener'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { defaultExternalUriOpenerId, ExternalUriOpenersConfiguration, externalUriOpenersSettingId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; +import { testUrlMatchesGlob } from 'vs/workbench/contrib/url/common/urlGlob'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; + + +export const IExternalUriOpenerService = createDecorator('externalUriOpenerService'); + + +export interface IExternalOpenerProvider { + getOpeners(targetUri: URI): AsyncIterable; +} + +export interface IExternalUriOpener { + readonly id: string; + readonly label: string; + + canOpen(uri: URI, token: CancellationToken): Promise; + openExternalUri(uri: URI, ctx: { sourceUri: URI }, token: CancellationToken): Promise; +} + +export interface IExternalUriOpenerService { + readonly _serviceBrand: undefined + + /** + * Registers a provider for external resources openers. + */ + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable; + + /** + * Get the configured IExternalUriOpener for the the uri. + * If there is no opener configured, then returns the first opener that can handle the uri. + */ + getOpener(uri: URI, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise; +} + +export class ExternalUriOpenerService extends Disposable implements IExternalUriOpenerService, IExternalOpener { + + public readonly _serviceBrand: undefined; + + private readonly _providers = new LinkedList(); + + constructor( + @IOpenerService openerService: IOpenerService, + @IStorageService storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILogService private readonly logService: ILogService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + ) { + super(); + this._register(openerService.registerExternalOpener(this)); + } + + registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable { + const remove = this._providers.push(provider); + return { dispose: remove }; + } + + private async getOpeners(targetUri: URI, allowOptional: boolean, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise { + const allOpeners = await this.getAllOpenersForUri(targetUri); + + if (allOpeners.size === 0) { + return []; + } + + // First see if we have a preferredOpener + if (ctx.preferredOpenerId) { + if (ctx.preferredOpenerId === defaultExternalUriOpenerId) { + return []; + } + + const preferredOpener = allOpeners.get(ctx.preferredOpenerId); + if (preferredOpener) { + // Skip the `canOpen` check here since the opener was specifically requested. + return [preferredOpener]; + } + } + + // Check to see if we have a configured opener + const configuredOpener = this.getConfiguredOpenerForUri(allOpeners, targetUri); + if (configuredOpener) { + // Skip the `canOpen` check here since the opener was specifically requested. + return configuredOpener === defaultExternalUriOpenerId ? [] : [configuredOpener]; + } + + // Then check to see if there is a valid opener + const validOpeners: Array<{ opener: IExternalUriOpener, priority: modes.ExternalUriOpenerPriority }> = []; + await Promise.all(Array.from(allOpeners.values()).map(async opener => { + let priority: modes.ExternalUriOpenerPriority; + try { + priority = await opener.canOpen(targetUri, token); + } catch (e) { + this.logService.error(e); + return; + } + + switch (priority) { + case modes.ExternalUriOpenerPriority.Option: + case modes.ExternalUriOpenerPriority.Default: + case modes.ExternalUriOpenerPriority.Preferred: + validOpeners.push({ opener, priority }); + break; + } + })); + + if (validOpeners.length === 0) { + return []; + } + + // See if we have a preferred opener first + const preferred = firstOrDefault(validOpeners.filter(x => x.priority === modes.ExternalUriOpenerPriority.Preferred)); + if (preferred) { + return [preferred.opener]; + } + + // See if we only have optional openers, use the default opener + if (!allowOptional && validOpeners.every(x => x.priority === modes.ExternalUriOpenerPriority.Option)) { + return []; + } + + return validOpeners.map(value => value.opener); + } + + async openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise { + + const targetUri = typeof href === 'string' ? URI.parse(href) : href; + + const allOpeners = await this.getOpeners(targetUri, false, ctx, token); + if (allOpeners.length === 0) { + return false; + } else if (allOpeners.length === 1) { + return allOpeners[0].openExternalUri(targetUri, ctx, token); + } + + // Otherwise prompt + return this.showOpenerPrompt(allOpeners, targetUri, ctx, token); + } + + async getOpener(targetUri: URI, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise { + const allOpeners = await this.getOpeners(targetUri, true, ctx, token); + if (allOpeners.length >= 1) { + return allOpeners[0]; + } + return undefined; + } + + private async getAllOpenersForUri(targetUri: URI): Promise> { + const allOpeners = new Map(); + await Promise.all(Iterable.map(this._providers, async (provider) => { + for await (const opener of provider.getOpeners(targetUri)) { + allOpeners.set(opener.id, opener); + } + })); + return allOpeners; + } + + private getConfiguredOpenerForUri(openers: Map, targetUri: URI): IExternalUriOpener | 'default' | undefined { + const config = this.configurationService.getValue(externalUriOpenersSettingId) || {}; + for (const [uriGlob, id] of Object.entries(config)) { + if (testUrlMatchesGlob(targetUri.toString(), uriGlob)) { + if (id === defaultExternalUriOpenerId) { + return 'default'; + } + + const entry = openers.get(id); + if (entry) { + return entry; + } + } + } + return undefined; + } + + private async showOpenerPrompt( + openers: ReadonlyArray, + targetUri: URI, + ctx: { sourceUri: URI }, + token: CancellationToken + ): Promise { + type PickItem = IQuickPickItem & { opener?: IExternalUriOpener | 'configureDefault' }; + + const items: Array = openers.map((opener): PickItem => { + return { + label: opener.label, + opener: opener + }; + }); + items.push( + { + label: isWeb + ? nls.localize('selectOpenerDefaultLabel.web', 'Open in new browser window') + : nls.localize('selectOpenerDefaultLabel', 'Open in default browser'), + opener: undefined + }, + { type: 'separator' }, + { + label: nls.localize('selectOpenerConfigureTitle', "Configure default opener..."), + opener: 'configureDefault' + }); + + const picked = await this.quickInputService.pick(items, { + placeHolder: nls.localize('selectOpenerPlaceHolder', "How would you like to open: {0}", targetUri.toString()) + }); + + if (!picked) { + // Still cancel the default opener here since we prompted the user + return true; + } + + if (typeof picked.opener === 'undefined') { + return false; // Fallback to default opener + } else if (picked.opener === 'configureDefault') { + await this.preferencesService.openGlobalSettings(true, { + revealSetting: { key: externalUriOpenersSettingId, edit: true } + }); + return true; + } else { + return picked.opener.openExternalUri(targetUri, ctx, token); + } + } +} diff --git a/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts new file mode 100644 index 0000000000..d37b4c53c5 --- /dev/null +++ b/src/vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExternalUriOpenerPriority } from 'vs/editor/common/modes'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IPickOptions, IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { ExternalUriOpenerService, IExternalOpenerProvider, IExternalUriOpener } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; + + +class MockQuickInputService implements Partial{ + + constructor( + private readonly pickIndex: number + ) { } + + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; + public async pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { + const resolvedPicks = await picks; + const item = resolvedPicks[this.pickIndex]; + if (item.type === 'separator') { + return undefined; + } + return item; + } + +} + +suite('ExternalUriOpenerService', () => { + + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = new TestInstantiationService(); + + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IOpenerService, { + registerExternalOpener: () => { return Disposable.None; } + }); + }); + + test('Should not open if there are no openers', async () => { + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + // noop + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, false); + }); + + test('Should prompt if there is at least one enabled opener', async () => { + instantiationService.stub(IQuickInputService, new MockQuickInputService(0)); + + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + let openedWithEnabled = false; + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + yield { + id: 'disabled-id', + label: 'disabled', + canOpen: async () => ExternalUriOpenerPriority.None, + openExternalUri: async () => true, + }; + yield { + id: 'enabled-id', + label: 'enabled', + canOpen: async () => ExternalUriOpenerPriority.Default, + openExternalUri: async () => { + openedWithEnabled = true; + return true; + } + }; + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, true); + assert.strictEqual(openedWithEnabled, true); + }); + + test('Should automatically pick single preferred opener without prompt', async () => { + const externalUriOpenerService: ExternalUriOpenerService = instantiationService.createInstance(ExternalUriOpenerService); + + let openedWithPreferred = false; + externalUriOpenerService.registerExternalOpenerProvider(new class implements IExternalOpenerProvider { + async *getOpeners(_targetUri: URI): AsyncGenerator { + yield { + id: 'other-id', + label: 'other', + canOpen: async () => ExternalUriOpenerPriority.Default, + openExternalUri: async () => { + return true; + } + }; + yield { + id: 'preferred-id', + label: 'preferred', + canOpen: async () => ExternalUriOpenerPriority.Preferred, + openExternalUri: async () => { + openedWithPreferred = true; + return true; + } + }; + } + }); + + const uri = URI.parse('http://contoso.com'); + const didOpen = await externalUriOpenerService.openExternal(uri.toString(), { sourceUri: uri }, CancellationToken.None); + assert.strictEqual(didOpen, true); + assert.strictEqual(openedWithPreferred, true); + }); +}); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index cef0211874..49177b3748 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -10,13 +10,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * An implementation of editor for binary files that cannot be displayed. @@ -29,9 +27,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IOpenerService private readonly openerService: IOpenerService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IQuickInputService private readonly quickInputService: IQuickInputService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, ) { @@ -55,7 +51,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { input.setForceOpenAsText(); // If more editors are installed that can handle this input, show a picker - await openEditorWith(input, undefined, options, this.group, this.editorService, this.configurationService, this.quickInputService); + await this.instantiationService.invokeFunction(openEditorWith, input, undefined, options, this.group); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index edd07597f9..b2fee1818b 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isFunction, assertIsDefined } from 'vs/base/common/types'; import { isValidBasename } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/resources'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -97,7 +97,7 @@ export class TextFileEditor extends BaseTextEditor { } getTitle(): string { - return this.input ? this.input.getName() : nls.localize('textFileEditor', "Text File Editor"); + return this.input ? this.input.getName() : localize('textFileEditor', "Text File Editor"); } get input(): FileEditorInput | undefined { @@ -169,22 +169,24 @@ export class TextFileEditor extends BaseTextEditor { if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { this.openAsFolder(input); - throw new Error(nls.localize('openFolderError', "File is a directory")); + throw new Error(localize('openFolderError', "File is a directory")); } // Offer to create a file from the error if we have a file not found and the name is valid if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.preferredResource))) { throw createErrorWithActions(toErrorMessage(error), { actions: [ - new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => { - await this.textFileService.create(input.preferredResource); + toAction({ + id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => { + await this.textFileService.create([{ resource: input.preferredResource }]); - return this.editorService.openEditor({ - resource: input.preferredResource, - options: { - pinned: true // new file gets pinned by default - } - }); + return this.editorService.openEditor({ + resource: input.preferredResource, + options: { + pinned: true // new file gets pinned by default + } + }); + } }) ] }); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index c007213d8e..efedf0bd1e 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -68,8 +68,8 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa } private registerListeners(): void { - this._register(this.textFileService.files.onDidSave(e => this.onFileSavedOrReverted(e.model.resource))); - this._register(this.textFileService.files.onDidRevert(m => this.onFileSavedOrReverted(m.resource))); + this._register(this.textFileService.files.onDidSave(event => this.onFileSavedOrReverted(event.model.resource))); + this._register(this.textFileService.files.onDidRevert(model => this.onFileSavedOrReverted(model.resource))); this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); } @@ -248,14 +248,14 @@ class ResolveSaveConflictAction extends Action { await TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }); // Show additional help how to resolve the save conflict - const actions: INotificationActions = { primary: [this.instantiationService.createInstance(ResolveConflictLearnMoreAction)] }; + const actions = { primary: [this.instantiationService.createInstance(ResolveConflictLearnMoreAction)] }; const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions, neverShowAgain: { id: LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, isSecondary: true } }); - Event.once(handle.onDidClose)(() => dispose(actions.primary!)); + Event.once(handle.onDidClose)(() => dispose(actions.primary)); pendingResolveSaveConflictMessages.push(handle); } } @@ -272,7 +272,7 @@ class SaveElevatedAction extends Action { async run(): Promise { if (!this.model.isDisposed()) { - this.model.save({ + await this.model.save({ writeElevated: true, overwriteReadonly: this.triedToMakeWriteable, reason: SaveReason.EXPLICIT @@ -291,7 +291,7 @@ class OverwriteReadonlyAction extends Action { async run(): Promise { if (!this.model.isDisposed()) { - this.model.save({ overwriteReadonly: true, reason: SaveReason.EXPLICIT }); + await this.model.save({ overwriteReadonly: true, reason: SaveReason.EXPLICIT }); } } } @@ -306,7 +306,7 @@ class SaveIgnoreModifiedSinceAction extends Action { async run(): Promise { if (!this.model.isDisposed()) { - this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); + await this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); } } } diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index 8c34a9588a..50a8af1251 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -10,7 +10,7 @@ import { IFilesConfiguration, SortOrder } from 'vs/workbench/contrib/files/commo import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileService, FileChangesEvent, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; +import { dirname, basename } from 'vs/base/common/resources'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -19,8 +19,9 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IBulkEditService, ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { IExplorerView, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressNotificationOptions, IProgressCompositeOptions } from 'vs/platform/progress/common/progress'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { RunOnceScheduler } from 'vs/base/common/async'; export const UNDO_REDO_SOURCE = new UndoRedoSource(); @@ -35,6 +36,8 @@ export class ExplorerService implements IExplorerService { private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; + private onFileChangesScheduler: RunOnceScheduler; + private fileChangeEvents: FileChangesEvent[] = []; constructor( @IFileService private fileService: IFileService, @@ -51,7 +54,51 @@ export class ExplorerService implements IExplorerService { this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService); this.disposables.add(this.model); this.disposables.add(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); - this.disposables.add(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + + this.onFileChangesScheduler = new RunOnceScheduler(async () => { + const events = this.fileChangeEvents; + this.fileChangeEvents = []; + + // Filter to the ones we care + const types = [FileChangeType.DELETED]; + if (this._sortOrder === SortOrder.Modified) { + types.push(FileChangeType.UPDATED); + } + + let shouldRefresh = false; + // For DELETED and UPDATED events go through the explorer model and check if any of the items got affected + this.roots.forEach(r => { + if (this.view && !shouldRefresh) { + shouldRefresh = doesFileEventAffect(r, this.view, events, types); + } + }); + // For ADDED events we need to go through all the events and check if the explorer is already aware of some of them + // Or if they affect not yet resolved parts of the explorer. If that is the case we will not refresh. + events.forEach(e => { + if (!shouldRefresh) { + const added = e.getAdded(); + if (added.some(a => { + const parent = this.model.findClosest(dirname(a.resource)); + // Parent of the added resource is resolved and the explorer model is not aware of the added resource - we need to refresh + return parent && !parent.getChild(basename(a.resource)); + })) { + shouldRefresh = true; + } + } + }); + + if (shouldRefresh) { + await this.refresh(false); + } + + }, ExplorerService.EXPLORER_FILE_CHANGES_REACT_DELAY); + + this.disposables.add(this.fileService.onDidFilesChange(e => { + this.fileChangeEvents.push(e); + if (!this.onFileChangesScheduler.isScheduled()) { + this.onFileChangesScheduler.schedule(); + } + })); this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); this.disposables.add(Event.any<{ scheme: string }>(this.fileService.onDidChangeFileSystemProviderRegistrations, this.fileService.onDidChangeFileSystemProviderCapabilities)(async e => { let affected = false; @@ -63,7 +110,7 @@ export class ExplorerService implements IExplorerService { }); if (affected) { if (this.view) { - await this.view.refresh(true); + await this.view.setTreeInput(); } } })); @@ -93,19 +140,20 @@ export class ExplorerService implements IExplorerService { return this.view.getContext(respectMultiSelection); } - async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string }): Promise { + async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string, confirmBeforeUndo?: boolean, progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise { const cancellationTokenSource = new CancellationTokenSource(); - const promise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 500, + const promise = this.progressService.withProgress({ + location: options.progressLocation || ProgressLocation.Window, title: options.progressLabel, - cancellable: edit.length > 1 // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. + cancellable: edit.length > 1, // Only allow cancellation when there is more than one edit. Since cancelling will not actually stop the current edit that is in progress. + delay: 500, }, async progress => { await this.bulkEditService.apply(edit, { undoRedoSource: UNDO_REDO_SOURCE, label: options.undoLabel, progress, - token: cancellationTokenSource.token + token: cancellationTokenSource.token, + confirmBeforeUndo: options.confirmBeforeUndo }); }, () => cancellationTokenSource.cancel()); await this.progressService.withProgress({ location: ProgressLocation.Explorer, delay: 500 }, () => promise); @@ -296,32 +344,6 @@ export class ExplorerService implements IExplorerService { } } - private onDidFilesChange(e: FileChangesEvent): void { - // Check if an explorer refresh is necessary (delayed to give internal events a chance to react first) - // Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might - // be fired first over the other or not at all. - setTimeout(async () => { - // Filter to the ones we care - const types = [FileChangeType.ADDED, FileChangeType.DELETED]; - if (this._sortOrder === SortOrder.Modified) { - types.push(FileChangeType.UPDATED); - } - - const allResolvedDirectories: ExplorerItem[] = []; - this.roots.forEach(r => { - allResolvedDirectories.push(r); - if (this.view) { - getAllNonFilteredDescendants(r, allResolvedDirectories, this.view); - } - }); - - const shouldRefresh = allResolvedDirectories.some(r => e.affects(r.resource, ...types)); - if (shouldRefresh) { - await this.refresh(false); - } - }, ExplorerService.EXPLORER_FILE_CHANGES_REACT_DELAY); - } - private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { const configSortOrder = configuration?.explorer?.sortOrder || 'default'; if (this._sortOrder !== configSortOrder) { @@ -338,13 +360,19 @@ export class ExplorerService implements IExplorerService { } } -function getAllNonFilteredDescendants(item: ExplorerItem, result: ExplorerItem[], view: IExplorerView): void { +function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: FileChangesEvent[], types: FileChangeType[]): boolean { for (let [_name, child] of item.children) { if (view.isItemVisible(child)) { + if (events.some(e => e.contains(child.resource, ...types))) { + return true; + } if (child.isDirectory && child.isDirectoryResolved) { - result.push(child); - getAllNonFilteredDescendants(child, result, view); + if (doesFileEventAffect(child, view, events, types)) { + return true; + } } } } + + return false; } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 3e8b33b87f..1502cc5329 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -28,7 +28,8 @@ import { DelegatingEditorService } from 'vs/workbench/services/editor/browser/ed import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -41,6 +42,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; const explorerViewIcon = registerIcon('explorer-view-icon', Codicon.files, localize('explorerViewIcon', 'View icon of the explorer view.')); +const openEditorsViewIcon = registerIcon('open-editors-view-icon', Codicon.book, localize('openEditorsIcon', 'View icon of the open editors view.')); export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -111,12 +113,13 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor id: OpenEditorsView.ID, name: OpenEditorsView.NAME, ctorDescriptor: new SyncDescriptor(OpenEditorsView), - containerIcon: explorerViewIcon, + containerIcon: openEditorsViewIcon, order: 0, when: OpenEditorsVisibleContext, canToggleVisibility: true, canMoveView: true, - collapsed: true, + collapsed: false, + hideByDefault: true, focusCommand: { id: 'workbench.files.action.focusOpenEditorsView', keybindings: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_E) } @@ -146,6 +149,13 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor ctorDescriptor: new SyncDescriptor(ExplorerView), order: 1, canToggleVisibility: false, + openCommandActionDescriptor: { + id: VIEW_CONTAINER.id, + title: localize('explore', "Explorer"), + mnemonicTitle: localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_E }, + order: 0 + }, focusCommand: { id: 'workbench.explorer.fileView.focus' } @@ -207,7 +217,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { let delay = 0; const config = this.configurationService.getValue(); - const delayEditorOpeningInOpenedEditors = !!config.workbench.editor.enablePreview; // No need to delay if preview is disabled + const delayEditorOpeningInOpenedEditors = !!config.workbench?.editor?.enablePreview; // No need to delay if preview is disabled const activeGroup = this.editorGroupService.activeGroup; if (delayEditorOpeningInOpenedEditors && group === activeGroup && !activeGroup.previewEditor) { @@ -267,7 +277,7 @@ const viewContainerRegistry = Registry.as(Extensions.Vi */ export const VIEW_CONTAINER: ViewContainer = viewContainerRegistry.registerViewContainer({ id: VIEWLET_ID, - name: localize('explore', "Explorer"), + title: localize('explore', "Explorer"), ctorDescriptor: new SyncDescriptor(ExplorerViewPaneContainer), storageId: 'workbench.explorer.views.state', icon: explorerViewIcon, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 27ec3fed6f..509031af08 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -5,12 +5,12 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; +import { ToggleAutoSaveAction, FocusFilesExplorer, GlobalCompareResourcesAction, ShowActiveFileInExplorer, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL, ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, NEW_UNTITLED_PLAIN_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_PLAIN_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -36,12 +36,9 @@ import { Codicon } from 'vs/base/common/codicons'; const category = { value: nls.localize('filesCategory', "File"), original: 'File' }; const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveAllAction, { primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) } }), 'File: Save All', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalCompareResourcesAction), 'File: Compare Active File With...', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(FocusFilesExplorer), 'File: Focus on Files Explorer', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowActiveFileInExplorer), 'File: Reveal Active File in Side Bar', category.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(CollapseExplorerView), 'File: Collapse Folders in Explorer', category.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RefreshExplorerView), 'File: Refresh Explorer', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(CompareWithClipboardAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleAutoSaveAction), 'File: Toggle Auto Save', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowOpenedFileInNewWindow, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category.value, EmptyWorkspaceSupportContext); @@ -231,6 +228,8 @@ appendToCommandPalette(NEW_UNTITLED_PLAIN_FILE_COMMAND_ID, { value: NEW_UNTITLED // Menu registration - open editors +const isFileOrUntitledResourceContextKey = ContextKeyExpr.or(ResourceContextKey.IsFileSystemResource, ResourceContextKey.Scheme.isEqualTo(Schemas.untitled)); + const openToSideCommand = { id: OPEN_TO_SIDE_COMMAND_ID, title: nls.localize('openToSide', "Open to the Side") @@ -239,7 +238,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 10, command: openToSideCommand, - when: ContextKeyExpr.or(ResourceContextKey.IsFileSystemResource, ResourceContextKey.Scheme.isEqualTo(Schemas.untitled)) + when: isFileOrUntitledResourceContextKey }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -330,7 +329,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 20, command: compareResourceCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.HasResource, ResourceSelectedForCompareContext, isFileOrUntitledResourceContextKey, WorkbenchListDoubleSelection.toNegated()) }); const selectForCompareCommand = { @@ -341,7 +340,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 30, command: selectForCompareCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.HasResource, isFileOrUntitledResourceContextKey, WorkbenchListDoubleSelection.toNegated()) }); const compareSelectedCommand = { @@ -352,7 +351,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '3_compare', order: 30, command: compareSelectedCommand, - when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection) + when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection, isFileOrUntitledResourceContextKey) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -637,7 +636,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '4_save', command: { - id: SaveAllAction.ID, + id: SAVE_ALL_COMMAND_ID, title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), precondition: DirtyWorkingCopiesContext }, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 90329846e4..83907c859c 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -15,13 +15,12 @@ import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/com import { VIEWLET_ID, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { ByteSize, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -83,40 +82,6 @@ async function refreshIfSeparator(value: string, explorerService: IExplorerServi } } -/* New File */ -export class NewFileAction extends Action { - static readonly ID = 'workbench.files.action.createFileFromExplorer'; - static readonly LABEL = nls.localize('createNewFile', "New File"); - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('explorer.newFile', NEW_FILE_LABEL); - this.class = 'explorer-action ' + Codicon.newFile.classNames; - } - - run(): Promise { - return this.commandService.executeCommand(NEW_FILE_COMMAND_ID); - } -} - -/* New Folder */ -export class NewFolderAction extends Action { - static readonly ID = 'workbench.files.action.createFolderFromExplorer'; - static readonly LABEL = nls.localize('createNewFolder', "New Folder"); - - constructor( - @ICommandService private commandService: ICommandService - ) { - super('explorer.newFolder', NEW_FOLDER_LABEL); - this.class = 'explorer-action ' + Codicon.newFolder.classNames; - } - - run(): Promise { - return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID); - } -} - async function deleteFiles(explorerService: IExplorerService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { @@ -168,6 +133,9 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer } let confirmation: IConfirmationResult; + // We do not support undo of folders, so in that case the delete action is irreversible + const deleteDetail = distinctElements.some(e => e.isDirectory) ? nls.localize('irreversible', "This action is irreversible!") : + distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command") : nls.localize('restore', "You can restore this file using the Undo command"); // Check if we need to ask for confirmation at all if (skipConfirm || (useTrash && configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false)) { @@ -199,7 +167,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer else { let { message, detail } = getDeleteMessage(distinctElements); detail += detail ? '\n' : ''; - detail += nls.localize('irreversible', "This action is irreversible!"); + detail += deleteDetail; confirmation = await dialogService.confirm({ message, detail, @@ -222,8 +190,8 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer try { const resourceFileEdits = distinctElements.map(e => new ResourceFileEdit(e.resource, undefined, { recursive: true, folder: e.isDirectory, skipTrashBin: !useTrash, maxSize: MAX_UNDO_FILE_SIZE })); const options = { - undoLabel: distinctElements.length > 1 ? nls.localize('deleteBulkEdit', "Delete {0} files", distinctElements.length) : nls.localize('deleteFileBulkEdit', "Delete {0}", distinctElements[0].name), - progressLabel: distinctElements.length > 1 ? nls.localize('deletingBulkEdit', "Deleting {0} files", distinctElements.length) : nls.localize('deletingFileBulkEdit', "Deleting {0}", distinctElements[0].name), + undoLabel: distinctElements.length > 1 ? nls.localize({ key: 'deleteBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Delete {0} files", distinctElements.length) : nls.localize({ key: 'deleteFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Delete {0}", distinctElements[0].name), + progressLabel: distinctElements.length > 1 ? nls.localize({ key: 'deletingBulkEdit', comment: ['Placeholder will be replaced by the number of files deleted'] }, "Deleting {0} files", distinctElements.length) : nls.localize({ key: 'deletingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file deleted'] }, "Deleting {0}", distinctElements[0].name), }; await explorerService.applyBulkEdit(resourceFileEdits, options); } catch (error) { @@ -234,7 +202,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer let primaryButton: string; if (useTrash) { errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); - detailMessage = nls.localize('irreversible', "This action is irreversible!"); + detailMessage = deleteDetail; primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); } else { errorMessage = toErrorMessage(error, false); @@ -411,6 +379,32 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa return `${name.substr(0, lastIndexOfDot)}.1${name.substr(lastIndexOfDot)}`; } + // 123 => 124 + let noNameNoExtensionRegex = RegExp('(\\d+)$'); + if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) { + return name.replace(noNameNoExtensionRegex, (match, g1?) => { + let number = parseInt(g1); + return number < maxNumber + ? String(number + 1).padStart(g1.length, '0') + : `${g1}.1`; + }); + } + + // file => file1 + // file1 => file2 + let noExtensionRegex = RegExp('(.*)(\\d*)$'); + if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) { + return name.replace(noExtensionRegex, (match, g1?, g2?) => { + let number = parseInt(g2); + if (isNaN(number)) { + number = 0; + } + return number < maxNumber + ? g1 + String(number + 1).padStart(g2.length, '0') + : `${g1}${g2}.1`; + }); + } + // folder.1=>folder.2 if (isFolder && name.match(/(\d+)$/)) { return name.replace(/(\d+)$/, (match, ...groups) => { @@ -560,20 +554,6 @@ export abstract class BaseSaveAllAction extends Action { } } -export class SaveAllAction extends BaseSaveAllAction { - - static readonly ID = 'workbench.action.files.saveAll'; - static readonly LABEL = SAVE_ALL_LABEL; - - get class(): string { - return 'explorer-action ' + Codicon.saveAll.classNames; - } - - protected doRun(): Promise { - return this.commandService.executeCommand(SAVE_ALL_COMMAND_ID); - } -} - export class SaveAllInGroupAction extends BaseSaveAllAction { static readonly ID = 'workbench.files.action.saveAllInGroup'; @@ -645,48 +625,6 @@ export class ShowActiveFileInExplorer extends Action { } } -export class CollapseExplorerView extends Action { - - static readonly ID = 'workbench.files.action.collapseExplorerFolders'; - static readonly LABEL = nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer"); - - constructor(id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService, - @IExplorerService readonly explorerService: IExplorerService - ) { - super(id, label, 'explorer-action ' + Codicon.collapseAll.classNames); - } - - async run(): Promise { - const explorerViewlet = (await this.viewletService.openViewlet(VIEWLET_ID))?.getViewPaneContainer() as ExplorerViewPaneContainer; - const explorerView = explorerViewlet.getExplorerView(); - if (explorerView) { - explorerView.collapseAll(); - } - } -} - -export class RefreshExplorerView extends Action { - - static readonly ID = 'workbench.files.action.refreshFilesExplorer'; - static readonly LABEL = nls.localize('refreshExplorer', "Refresh Explorer"); - - - constructor( - id: string, label: string, - @IViewletService private readonly viewletService: IViewletService, - @IExplorerService private readonly explorerService: IExplorerService - ) { - super(id, label, 'explorer-action ' + Codicon.refresh.classNames); - } - - async run(): Promise { - await this.viewletService.openViewlet(VIEWLET_ID); - await this.explorerService.refresh(); - } -} - export class ShowOpenedFileInNewWindow extends Action { static readonly ID = 'workbench.action.files.showOpenedFileInNewWindow'; @@ -914,8 +852,9 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole try { const resourceToCreate = resources.joinPath(folder.resource, value); await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], { - progressLabel: nls.localize('newBulkEdit', "New {0}", value), - undoLabel: nls.localize('newBulkEdit', "New {0}", value) + undoLabel: nls.localize('createBulkEdit', "Create {0}", value), + progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value), + confirmBeforeUndo: true }); await refreshIfSeparator(value, explorerService); @@ -1025,7 +964,7 @@ export const cutFileHandler = async (accessor: ServicesAccessor) => { }; export const DOWNLOAD_COMMAND_ID = 'explorer.download'; -const downloadFileHandler = (accessor: ServicesAccessor) => { +const downloadFileHandler = async (accessor: ServicesAccessor) => { const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); const fileDialogService = accessor.get(IFileDialogService); @@ -1037,7 +976,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { const cts = new CancellationTokenSource(); - const downloadPromise = progressService.withProgress({ + await progressService.withProgress({ location: ProgressLocation.Window, delay: 800, cancellable: isWeb, @@ -1242,7 +1181,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { const destination = await fileDialogService.showSaveDialog({ availableFileSystems: [Schemas.file], saveLabel: mnemonicButtonLabel(nls.localize('downloadButton', "Download")), - title: explorerItem.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + title: nls.localize('chooseWhereToDownload', "Choose Where to Download"), defaultUri }); @@ -1250,6 +1189,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { await explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { undoLabel: nls.localize('downloadBulkEdit', "Download {0}", explorerItem.name), progressLabel: nls.localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), + progressLocation: ProgressLocation.Explorer }); } else { cts.cancel(); // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 @@ -1257,9 +1197,6 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { } })); }, () => cts.dispose(true)); - - // Also indicate progress in the files view - progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => downloadPromise); }; CommandsRegistry.registerCommand({ @@ -1303,24 +1240,28 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { return { source: fileToPaste, target: targetFile }; })); - // Move/Copy File - if (pasteShouldMove) { - const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target)); - const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('movingBulkEdit', "Moving {0} files", sourceTargetPairs.length) : nls.localize('movingFileBulkEdit', "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('moveBulkEdit', "Move {0} files", sourceTargetPairs.length) : nls.localize('moveFileBulkEdit', "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) - }; - await explorerService.applyBulkEdit(resourceFileEdits, options); - } else { - const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true })); - const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('copyingBulkEdit', "Copying {0} files", sourceTargetPairs.length) : nls.localize('copyingFileBulkEdit', "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('copyBulkEdit', "Copy {0} files", sourceTargetPairs.length) : nls.localize('copyFileBulkEdit', "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) - }; - await explorerService.applyBulkEdit(resourceFileEdits, options); - } - if (sourceTargetPairs.length >= 1) { + // Move/Copy File + if (pasteShouldMove) { + const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target)); + const options = { + progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'movingBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Moving {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'movingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'moveBulkEdit', comment: ['Placeholder will be replaced by the number of files being moved'] }, "Move {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'moveFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file moved.'] }, "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + }; + await explorerService.applyBulkEdit(resourceFileEdits, options); + } else { + const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true })); + const options = { + progressLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyingBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copying {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'copyingFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize({ key: 'copyBulkEdit', comment: ['Placeholder will be replaced by the number of files being copied'] }, "Copy {0} files", sourceTargetPairs.length) + : nls.localize({ key: 'copyFileBulkEdit', comment: ['Placeholder will be replaced by the name of the file copied.'] }, "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + }; + await explorerService.applyBulkEdit(resourceFileEdits, options); + } + const pair = sourceTargetPairs[0]; await explorerService.select(pair.target); if (sourceTargetPairs.length === 1) { diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index c626aba734..07aca5e853 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -16,7 +16,7 @@ import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/ex import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IListService } from 'vs/platform/list/browser/listService'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -27,7 +27,7 @@ import { getResourceForCommand, getMultiSelectedResources, getOpenEditorsViewMul import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { getMultiSelectedEditorContexts } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Schemas } from 'vs/base/common/network'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IEditorService, SIDE_GROUP, ISaveEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService, GroupsOrder, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -40,11 +40,10 @@ import { coalesce } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { toAction } from 'vs/base/common/actions'; // Commands @@ -357,13 +356,11 @@ CommandsRegistry.registerCommand({ handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService)); if (uri) { const input = editorService.createEditorInput({ resource: uri }); - openEditorWith(input, undefined, undefined, editorGroupsService.activeGroup, editorService, configurationService, quickInputService); + openEditorWith(accessor, input, undefined, undefined, editorGroupsService.activeGroup); } } }); @@ -442,12 +439,23 @@ function saveDirtyEditorsOfGroups(accessor: ServicesAccessor, groups: ReadonlyAr async function doSaveEditors(accessor: ServicesAccessor, editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { const editorService = accessor.get(IEditorService); const notificationService = accessor.get(INotificationService); + const commandService = accessor.get(ICommandService); try { await editorService.save(editors, options); } catch (error) { if (!isPromiseCanceledError(error)) { - notificationService.error(nls.localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); + notificationService.notify({ + severity: Severity.Error, + message: nls.localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false)), + actions: { + primary: [ + toAction({ id: SAVE_FILE_COMMAND_ID, label: nls.localize('retry', "Retry"), run: () => commandService.executeCommand(SAVE_FILE_COMMAND_ID) }), + toAction({ id: SAVE_FILE_AS_COMMAND_ID, label: SAVE_FILE_AS_LABEL, run: () => commandService.executeCommand(SAVE_FILE_AS_COMMAND_ID) }), + toAction({ id: REVERT_FILE_COMMAND_ID, label: nls.localize('discard', "Discard"), run: () => commandService.executeCommand(REVERT_FILE_COMMAND_ID) }) + ] + } + }); } } } @@ -483,7 +491,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -CommandsRegistry.registerCommand({ +KeybindingsRegistry.registerCommandAndKeybindingRule({ + when: undefined, + weight: KeybindingWeight.WorkbenchContrib, + primary: undefined, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, + win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) }, id: SAVE_ALL_COMMAND_ID, handler: (accessor) => { return saveDirtyEditorsOfGroups(accessor, accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), { reason: SaveReason.EXPLICIT }); @@ -665,12 +678,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (typeof args?.viewType === 'string') { const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); const textInput = editorService.createEditorInput({ options: { pinned: true } }); const group = editorGroupsService.activeGroup; - await openEditorWith(textInput, args.viewType, { pinned: true }, group, editorService, configurationService, quickInputService); + await openEditorWith(accessor, textInput, args.viewType, { pinned: true }, group); } else { await editorService.openEditor({ options: { pinned: true } }); // untitled are always pinned } @@ -686,12 +697,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (viewType) { const editorGroupsService = accessor.get(IEditorGroupsService); - const configurationService = accessor.get(IConfigurationService); - const quickInputService = accessor.get(IQuickInputService); - const textInput = editorService.createEditorInput({ options: { pinned: true }, mode: 'txt' }); const group = editorGroupsService.activeGroup; - await openEditorWith(textInput, viewType, { pinned: true }, group, editorService, configurationService, quickInputService); + await openEditorWith(accessor, textInput, NEW_UNTITLED_PLAIN_FILE_COMMAND_ID, { pinned: true }, group); } else { await editorService.openEditor({ options: { pinned: true }, mode: 'txt' }); // untitled are always pinned } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index d5a679b819..b5264216be 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -3,35 +3,25 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; -import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import * as nls from 'vs/nls'; import { sep } from 'vs/base/common/path'; -import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; +import { EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, FILES_EXCLUDE_CONFIG, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; -import { VIEWLET_ID, SortOrder, FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; +import { SortOrder, FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; import { ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExplorerService, UNDO_REDO_SOURCE } from 'vs/workbench/contrib/files/browser/explorerService'; import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; @@ -39,26 +29,9 @@ import { Schemas } from 'vs/base/common/network'; import { WorkspaceWatcher } from 'vs/workbench/contrib/files/common/workspaceWatcher'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; import { DirtyFilesIndicator } from 'vs/workbench/contrib/files/common/dirtyFilesIndicator'; -import { isEqual } from 'vs/base/common/resources'; import { UndoCommand, RedoCommand } from 'vs/editor/browser/editorExtensions'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; - -// Viewlet Action -export class OpenExplorerViewletAction extends ShowViewletAction { - static readonly ID = VIEWLET_ID; - static readonly LABEL = nls.localize('showExplorerViewlet', "Show Explorer"); - - constructor( - id: string, - label: string, - @IViewletService viewletService: IViewletService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService); - } -} +import { FileEditorInputFactory, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; class FileUriLabelContribution implements IWorkbenchContribution { @@ -79,18 +52,6 @@ class FileUriLabelContribution implements IWorkbenchContribution { registerSingleton(IExplorerService, ExplorerService, true); -const openViewletKb: IKeybindings = { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_E -}; - -// Register Action to Open Viewlet -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction( - SyncActionDescriptor.from(OpenExplorerViewletAction, openViewletKb), - 'View: Show Explorer', - CATEGORIES.View.value -); - // Register file editors Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -115,58 +76,7 @@ Registry.as(EditorInputExtensions.EditorInputFactor } }); -interface ISerializedFileEditorInput { - resourceJSON: UriComponents; - preferredResourceJSON?: UriComponents; - name?: string; - description?: string; - encoding?: string; - modeId?: string; -} - // Register Editor Input Factory -class FileEditorInputFactory implements IEditorInputFactory { - - canSerialize(editorInput: EditorInput): boolean { - return true; - } - - serialize(editorInput: EditorInput): string { - const fileEditorInput = editorInput; - const resource = fileEditorInput.resource; - const preferredResource = fileEditorInput.preferredResource; - const serializedFileEditorInput: ISerializedFileEditorInput = { - resourceJSON: resource.toJSON(), - preferredResourceJSON: isEqual(resource, preferredResource) ? undefined : preferredResource, // only storing preferredResource if it differs from the resource - name: fileEditorInput.getPreferredName(), - description: fileEditorInput.getPreferredDescription(), - encoding: fileEditorInput.getEncoding(), - modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data - }; - - return JSON.stringify(serializedFileEditorInput); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput { - return instantiationService.invokeFunction(accessor => { - const serializedFileEditorInput: ISerializedFileEditorInput = JSON.parse(serializedEditorInput); - const resource = URI.revive(serializedFileEditorInput.resourceJSON); - const preferredResource = URI.revive(serializedFileEditorInput.preferredResourceJSON); - const name = serializedFileEditorInput.name; - const description = serializedFileEditorInput.description; - const encoding = serializedFileEditorInput.encoding; - const mode = serializedFileEditorInput.modeId; - - const fileEditorInput = accessor.get(IEditorService).createEditorInput({ resource, label: name, description, encoding, mode, forceFile: true }) as FileEditorInput; - if (preferredResource) { - fileEditorInput.setPreferredResource(preferredResource); - } - - return fileEditorInput; - }); - } -} - Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(FILE_EDITOR_INPUT_ID, FileEditorInputFactory); // Register Explorer views @@ -198,8 +108,8 @@ const hotExitConfiguration: IConfigurationPropertySchema = platform.isNative ? 'default': HotExitConfiguration.ON_EXIT, 'markdownEnumDescriptions': [ nls.localize('hotExit.off', 'Disable hot exit. A prompt will show when attempting to close a window with dirty files.'), - nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`'), - nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of workspaces with unsaved files can be accessed via `File > Open Recent > More...`') + nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`'), + nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. A list of previously opened windows with unsaved files can be accessed via `File > Open Recent > More...`') ], 'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) } : { @@ -386,7 +296,7 @@ configurationRegistry.registerConfiguration({ nls.localize({ key: 'everything', comment: ['This is the description of an option'] }, "Format the whole file."), nls.localize({ key: 'modification', comment: ['This is the description of an option'] }, "Format modifications (requires source control)."), ], - 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is `true`."), + 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is enabled."), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, }, } @@ -479,21 +389,10 @@ configurationRegistry.registerConfiguration({ } }); -// View menu -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '3_views', - command: { - id: VIEWLET_ID, - title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer") - }, - // {{SQL CARBON EDIT}} - Change the order - order: 3 -}); - UndoCommand.addImplementation(110, (accessor: ServicesAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); const explorerService = accessor.get(IExplorerService); - if (explorerService.hasViewFocus()) { + if (explorerService.hasViewFocus() && undoRedoService.canUndo(UNDO_REDO_SOURCE)) { undoRedoService.undo(UNDO_REDO_SOURCE); return true; } @@ -504,7 +403,7 @@ UndoCommand.addImplementation(110, (accessor: ServicesAccessor) => { RedoCommand.addImplementation(110, (accessor: ServicesAccessor) => { const undoRedoService = accessor.get(IUndoRedoService); const explorerService = accessor.get(IExplorerService); - if (explorerService.hasViewFocus()) { + if (explorerService.hasViewFocus() && undoRedoService.canRedo(UNDO_REDO_SOURCE)) { undoRedoService.redo(UNDO_REDO_SOURCE); return true; } diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index bb404e73f5..ffe04c33e5 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; import { OpenEditor, SortOrder } from 'vs/workbench/contrib/files/common/files'; -import { EditorResourceAccessor, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SideBySideEditor, IEditorIdentifier, EditorInput, IEditorInputFactory } from 'vs/workbench/common/editor'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; @@ -14,9 +14,62 @@ import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditableData } from 'vs/workbench/common/views'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { isEqual } from 'vs/base/common/resources'; +interface ISerializedFileEditorInput { + resourceJSON: UriComponents; + preferredResourceJSON?: UriComponents; + name?: string; + description?: string; + encoding?: string; + modeId?: string; +} + +export class FileEditorInputFactory implements IEditorInputFactory { + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string { + const fileEditorInput = editorInput; + const resource = fileEditorInput.resource; + const preferredResource = fileEditorInput.preferredResource; + const serializedFileEditorInput: ISerializedFileEditorInput = { + resourceJSON: resource.toJSON(), + preferredResourceJSON: isEqual(resource, preferredResource) ? undefined : preferredResource, // only storing preferredResource if it differs from the resource + name: fileEditorInput.getPreferredName(), + description: fileEditorInput.getPreferredDescription(), + encoding: fileEditorInput.getEncoding(), + modeId: fileEditorInput.getPreferredMode() // only using the preferred user associated mode here if available to not store redundant data + }; + + return JSON.stringify(serializedFileEditorInput); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput { + return instantiationService.invokeFunction(accessor => { + const serializedFileEditorInput: ISerializedFileEditorInput = JSON.parse(serializedEditorInput); + const resource = URI.revive(serializedFileEditorInput.resourceJSON); + const preferredResource = URI.revive(serializedFileEditorInput.preferredResourceJSON); + const name = serializedFileEditorInput.name; + const description = serializedFileEditorInput.description; + const encoding = serializedFileEditorInput.encoding; + const mode = serializedFileEditorInput.modeId; + + const fileEditorInput = accessor.get(IEditorService).createEditorInput({ resource, label: name, description, encoding, mode, forceFile: true }) as FileEditorInput; + if (preferredResource) { + fileEditorInput.setPreferredResource(preferredResource); + } + + return fileEditorInput; + }); + } +} export interface IExplorerService { readonly _serviceBrand: undefined; @@ -34,7 +87,7 @@ export interface IExplorerService { refresh(): Promise; setToCopy(stats: ExplorerItem[], cut: boolean): Promise; isCut(stat: ExplorerItem): boolean; - applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string }): Promise; + applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string, progressLabel: string, confirmBeforeUndo?: boolean, progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise; /** * Selects and reveal the file element provided by the given resource if its found in the explorer. diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 995dd496b0..42f38edb04 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -89,7 +89,7 @@ height: 22px; } -.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .wrapper > .input { +.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .ibwrapper > .input { padding: 0; height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 491eccd0ef..44681107ac 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -11,7 +11,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index df20f84d99..dcdd93a101 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,30 +8,30 @@ import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext } from 'vs/workbench/contrib/files/common/files'; -import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView, CollapseExplorerView } from 'vs/workbench/contrib/files/browser/fileActions'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, ExplorerResourceAvailableEditorIdsContext, VIEW_ID, VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; +import { FileCopiedContext, NEW_FILE_COMMAND_ID, NEW_FOLDER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileActions'; import * as DOM from 'vs/base/browser/dom'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ExplorerDecorationsProvider } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewPaneOptions, ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; import { ExplorerDelegate, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate, isCompressedFolderName } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; @@ -52,6 +52,9 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { Codicon } from 'vs/base/common/codicons'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -75,6 +78,16 @@ function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree { + if (stat instanceof NewExplorerItem) { + return `new:${stat.resource}`; + } + + return stat.resource; + } +}; + export function getContext(focus: ExplorerItem[], selection: ExplorerItem[], respectMultiSelection: boolean, compressedNavigationControllerProvider: { getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined }): ExplorerItem[] { @@ -146,7 +159,6 @@ export class ExplorerView extends ViewPane { private shouldRefresh = true; private dragHandler!: DelayedDragHandler; private autoReveal: boolean | 'focusNoScroll' = false; - private actions: IAction[] | undefined; private decorationsProvider: ExplorerDecorationsProvider | undefined; constructor( @@ -284,19 +296,6 @@ export class ExplorerView extends ViewPane { })); } - getActions(): IAction[] { - if (!this.actions) { - this.actions = [ - this.instantiationService.createInstance(NewFileAction), - this.instantiationService.createInstance(NewFolderAction), - this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL), - this.instantiationService.createInstance(CollapseExplorerView, CollapseExplorerView.ID, CollapseExplorerView.LABEL) - ]; - this.actions.forEach(a => this._register(a)); - } - return this.actions; - } - focus(): void { this.tree.domFocus(); @@ -381,15 +380,7 @@ export class ExplorerView extends ViewPane { this.instantiationService.createInstance(ExplorerDataSource), { compressionEnabled: isCompressionEnabled(), accessibilityProvider: this.renderer, - identityProvider: { - getId: (stat: ExplorerItem) => { - if (stat instanceof NewExplorerItem) { - return `new:${stat.resource}`; - } - - return stat.resource; - } - }, + identityProvider, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (stat: ExplorerItem) => { if (this.explorerService.isEditable(stat)) { @@ -593,7 +584,9 @@ export class ExplorerView extends ViewPane { } const toRefresh = item || this.tree.getInput(); - return this.tree.updateChildren(toRefresh, recursive); + return this.tree.updateChildren(toRefresh, recursive, false, { + diffIdentityProvider: identityProvider + }); } focusNeighbourIfItemFocused(item: ExplorerItem): void { @@ -635,7 +628,7 @@ export class ExplorerView extends ViewPane { const initialInputSetup = !this.tree.getInput(); if (initialInputSetup) { - perf.mark('willResolveExplorer'); + perf.mark('code/willResolveExplorer'); } const roots = this.explorerService.roots; let input: ExplorerItem | ExplorerItem[] = roots[0]; @@ -675,7 +668,7 @@ export class ExplorerView extends ViewPane { } } if (initialInputSetup) { - perf.mark('didResolveExplorer'); + perf.mark('code/didResolveExplorer'); } }); @@ -856,3 +849,93 @@ function createFileIconThemableTreeContainerScope(container: HTMLElement, themeS onDidChangeFileIconTheme(themeService.getFileIconTheme()); return themeService.onDidFileIconThemeChange(onDidChangeFileIconTheme); } + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.createFileFromExplorer', + title: nls.localize('createNewFile', "New File"), + f1: false, + icon: Codicon.newFile, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 10 + } + }); + } + + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(NEW_FILE_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.createFolderFromExplorer', + title: nls.localize('createNewFolder', "New Folder"), + f1: false, + icon: Codicon.newFolder, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 20 + } + }); + } + + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand(NEW_FOLDER_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.files.action.refreshFilesExplorer', + title: nls.localize('refreshExplorer', "Refresh Explorer"), + f1: true, + icon: Codicon.refresh, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 30 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const viewletService = accessor.get(IViewletService); + const explorerService = accessor.get(IExplorerService); + await viewletService.openViewlet(VIEWLET_ID); + await explorerService.refresh(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.files.action.collapseExplorerFolders', + title: nls.localize('collapseExplorerFolders', "Collapse Folders in Explorer"), + viewId: VIEW_ID, + f1: true, + icon: Codicon.collapseAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', VIEW_ID), + order: 40 + } + }); + } + + runInView(_accessor: ServicesAccessor, view: ExplorerView): void { + view.collapseAll(); + } +}); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index e446600dbe..7b2006bbb2 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -20,7 +20,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; -import { dirname, joinPath, basename, distinctParents, basenameOrAuthority } from 'vs/base/common/resources'; +import { dirname, joinPath, basename, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -451,7 +451,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { if (e.equals(KeyCode.Enter)) { - if (inputBox.validate()) { + if (!inputBox.validate()) { done(true, true); } } else if (e.equals(KeyCode.Escape)) { @@ -626,7 +626,7 @@ export class FilesFilter implements ITreeFilter { stat.isExcluded = true; return false; } - if (this.explorerService.getEditableData(stat) || stat.isRoot) { + if (this.explorerService.getEditableData(stat)) { return true; // always visible } @@ -982,7 +982,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { + async drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): Promise { this.compressedDropTargetDisposable.dispose(); // Find compressed target @@ -1013,24 +1013,28 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (data instanceof NativeDragAndDropData) { const cts = new CancellationTokenSource(); - // Indicate progress globally - try { - if (isWeb) { - const dropPromise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: true, - title: localize('uploadingFiles', "Uploading") - }, async progress => { - this.handleWebExternalDrop(resolvedTarget, originalEvent, progress, cts.token); - }, () => cts.dispose(true)); - // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => dropPromise); - } else { - this.handleExternalDrop(resolvedTarget, originalEvent, cts.token); + if (isWeb) { + // Indicate progress globally + const dropPromise = this.progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('uploadingFiles', "Uploading") + }, async progress => { + try { + await this.handleWebExternalDrop(resolvedTarget, originalEvent, progress, cts.token); + } catch (error) { + this.notificationService.warn(error); + } + }, () => cts.dispose(true)); + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => dropPromise); + } else { + try { + await this.handleExternalDrop(resolvedTarget, originalEvent, cts.token); + } catch (error) { + this.notificationService.warn(error); } - } catch (error) { - this.notificationService.warn(error); } } // In-Explorer DND (Move/Copy file) @@ -1074,7 +1078,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true })], { - undoLabel: localize('overwrite', "Overwriting {0}", entry.name), + undoLabel: localize('overwrite', "Overwrite {0}", entry.name), progressLabel: localize('overwriting', "Overwriting {0}", entry.name), }); @@ -1345,14 +1349,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); await this.explorerService.applyBulkEdit(resourceFileEdits, { - undoLabel: resourcesFiltered.length === 1 ? localize('copyFile', "Copy {0}", basename(resourcesFiltered[0])) : localize('copynFile', "Copy {0} files", resourcesFiltered.length), - progressLabel: resourcesFiltered.length === 1 ? localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : localize('copyingnFile', "Copying {0} files", resourcesFiltered.length) + undoLabel: resourcesFiltered.length === 1 ? localize('copyFile', "Copy {0}", basename(resourcesFiltered[0])) : localize('copynFile', "Copy {0} resources", resourcesFiltered.length), + progressLabel: resourcesFiltered.length === 1 ? localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : localize('copyingnFile', "Copying {0} resources", resourcesFiltered.length) }); // if we only add one file, just open it directly if (resourceFileEdits.length === 1) { const item = this.explorerService.findClosest(resourceFileEdits[0].newResource!); - if (item) { + if (item && !item.isDirectory) { this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); } } @@ -1440,9 +1444,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Reuse duplicate action when user copies const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming; const resourceFileEdits = sources.map(({ resource, isDirectory }) => (new ResourceFileEdit(resource, findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming), { copy: true }))); + const labelSufix = getFileOrFolderLabelSufix(sources); await this.explorerService.applyBulkEdit(resourceFileEdits, { - undoLabel: resourceFileEdits.length > 1 ? localize('copy', "Copy {0} files", resourceFileEdits.length) : localize('copyOneFile', "Copy {0}", basenameOrAuthority(resourceFileEdits[0].newResource!)), - progressLabel: resourceFileEdits.length > 1 ? localize('copying', "Copying {0} files", resourceFileEdits.length) : localize('copyingOneFile', "Copying {0}", basenameOrAuthority(resourceFileEdits[0].newResource!)), + undoLabel: localize('copy', "Copy {0}", labelSufix), + progressLabel: localize('copying', "Copying {0}", labelSufix), }); const editors = resourceFileEdits.filter(edit => { @@ -1457,9 +1462,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Do not allow moving readonly items const resourceFileEdits = sources.filter(source => !source.isReadonly).map(source => new ResourceFileEdit(source.resource, joinPath(target.resource, source.name))); + const labelSufix = getFileOrFolderLabelSufix(sources); const options = { - undoLabel: sources.length > 1 ? localize('move', "Move {0} files", sources.length) : localize('moveOneFile', "Move {0}", sources[0].name), - progressLabel: sources.length > 1 ? localize('moving', "Moving {0} files", sources.length) : localize('movingOneFile', "Moving {0}", sources[0].name), + undoLabel: localize('move', "Move {0}", labelSufix), + progressLabel: localize('moving', "Moving {0}", labelSufix) }; try { @@ -1564,3 +1570,18 @@ export class ExplorerCompressionDelegate implements ITreeCompressionDelegate i.isDirectory)) { + return `${items.length} folders`; + } + if (items.every(i => !i.isDirectory)) { + return `${items.length} files`; + } + + return `${items.length} files and folders`; +} diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 941d477480..66a7ed20ac 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -9,16 +9,15 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorInput, Verbosity, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; -import { SaveAllAction, SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; +import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files'; import { CloseAllEditorsAction, CloseEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/layoutActions'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -30,11 +29,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; @@ -49,6 +48,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { compareFileNamesDefault } from 'vs/base/common/comparers'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const $ = dom.$; @@ -92,14 +95,26 @@ export class OpenEditorsView extends ViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.structuralRefreshDelay = 0; + let labelChangeListeners: IDisposable[] = []; this.listRefreshScheduler = new RunOnceScheduler(() => { + labelChangeListeners = dispose(labelChangeListeners); const previousLength = this.list.length; - this.list.splice(0, this.list.length, this.getElements()); + const elements = this.getElements(); + this.list.splice(0, this.list.length, elements); this.focusActiveEditor(); if (previousLength !== this.list.length) { this.updateSize(); } this.needsRefresh = false; + + if (this.sortOrder === 'alphabetical') { + // We need to resort the list if the editor label changed + elements.forEach(e => { + if (e instanceof OpenEditor) { + labelChangeListeners.push(e.editor.onDidChangeLabel(() => this.listRefreshScheduler.schedule())); + } + }); + } }, this.structuralRefreshDelay); this.sortOrder = configurationService.getValue('explorer.openEditors.sortOrder'); @@ -151,6 +166,7 @@ export class OpenEditorsView extends ViewPane { case GroupChangeKind.EDITOR_STICKY: case GroupChangeKind.EDITOR_PIN: { this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]); + this.focusActiveEditor(); break; } case GroupChangeKind.EDITOR_OPEN: @@ -267,20 +283,16 @@ export class OpenEditorsView extends ViewPane { })); const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); this._register(resourceNavigator.onDidOpen(e => { - if (typeof e.element !== 'number') { + if (!e.element) { return; - } - - const element = this.list.element(e.element); - - if (element instanceof OpenEditor) { + } else if (e.element instanceof OpenEditor) { if (e.browserEvent instanceof MouseEvent && e.browserEvent.button === 1) { return; // middle click already handled above: closes the editor } - this.openEditor(element, { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned, sideBySide: e.sideBySide }); + this.openEditor(e.element, { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned, sideBySide: e.sideBySide }); } else { - this.editorGroupService.activateGroup(element); + this.editorGroupService.activateGroup(e.element); } })); @@ -298,14 +310,6 @@ export class OpenEditorsView extends ViewPane { })); } - getActions(): IAction[] { - return [ - this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL), - this.instantiationService.createInstance(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL), - this.instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL) - ]; - } - focus(): void { super.focus(); this.list.domFocus(); @@ -709,3 +713,86 @@ class OpenEditorsAccessibilityProvider implements IListAccessibilityProvider { + const editorGroupService = accessor.get(IEditorGroupsService); + const newOrientation = (editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; + editorGroupService.setGroupOrientation(newOrientation); + } +}); + +MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: 'z_flip', + command: { + id: toggleEditorGroupLayoutId, + title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") + }, + order: 1 +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.files.saveAll', + title: SAVE_ALL_LABEL, + f1: true, + icon: Codicon.saveAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', OpenEditorsView.ID), + order: 20 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand(SAVE_ALL_COMMAND_ID); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'openEditors.closeAll', + title: CloseAllEditorsAction.LABEL, + f1: false, + icon: Codicon.closeAll, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + when: ContextKeyEqualsExpr.create('view', OpenEditorsView.ID), + order: 30 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + const closeAll = instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL); + await closeAll.run(); + } +}); diff --git a/src/vs/workbench/contrib/files/common/workspaceWatcher.ts b/src/vs/workbench/contrib/files/common/workspaceWatcher.ts index e4eb6349cd..92ea88b6e1 100644 --- a/src/vs/workbench/contrib/files/common/workspaceWatcher.ts +++ b/src/vs/workbench/contrib/files/common/workspaceWatcher.ts @@ -89,7 +89,7 @@ export class WorkspaceWatcher extends Disposable { if (msg.indexOf('ENOSPC') >= 0) { this.notificationService.prompt( Severity.Warning, - localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."), + localize('enospcError', "Unable to watch for file changes in this large workspace folder. Please follow the instructions link to resolve this issue."), [{ label: localize('learnMore', "Instructions"), run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693')) diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index 61c9c4ee09..d6224e2e78 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { EditorOptions } from 'vs/workbench/common/editor'; import { FileOperationError, FileOperationResult, IFileService, MIN_MAX_MEMORY_SIZE_MB, FALLBACK_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -56,18 +56,22 @@ export class NativeTextFileEditor extends TextFileEditor { if ((error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - throw createErrorWithActions(nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { + throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { actions: [ - new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => { - return this.nativeHostService.relaunch({ - addArgs: [ - `--max-memory=${memoryLimit}` - ] - }); + toAction({ + id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { + return this.nativeHostService.relaunch({ + addArgs: [ + `--max-memory=${memoryLimit}` + ] + }); + } + }), + toAction({ + id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { + return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); + } }), - new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => { - return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); - }) ] }); } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index 2a4518de1b..11edb19596 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -7,16 +7,10 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { TestFilesConfigurationService, workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestFilesConfigurationService, workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; @@ -29,25 +23,17 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe suite('EditorAutoSave', () => { - let disposables: IDisposable[] = []; + const disposables = new DisposableStore(); setup(() => { - disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TextFileEditor, - TextFileEditor.ID, - 'Text File Editor' - ), - [new SyncDescriptor(FileEditorInput)] - )); + disposables.add(registerTestFileEditor()); }); teardown(() => { - dispose(disposables); - disposables = []; + disposables.clear(); }); - async function createEditorAutoSave(autoSaveConfig: object): Promise<[TestServiceAccessor, EditorPart, EditorAutoSave]> { + async function createEditorAutoSave(autoSaveConfig: object): Promise { const instantiationService = workbenchInstantiationService(); const configurationService = new TestConfigurationService(); @@ -59,7 +45,7 @@ suite('EditorAutoSave', () => { configurationService )); - const part = instantiationService.createInstance(EditorPart); + const part = disposables.add(instantiationService.createInstance(EditorPart)); part.create(document.createElement('div')); part.layout(400, 300); @@ -69,14 +55,15 @@ suite('EditorAutoSave', () => { instantiationService.stub(IEditorService, editorService); const accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add((accessor.textFileService.files)); - const editorAutoSave = instantiationService.createInstance(EditorAutoSave); + disposables.add(instantiationService.createInstance(EditorAutoSave)); - return [accessor, part, editorAutoSave]; + return accessor; } test('editor auto saves after short delay if configured', async function () { - const [accessor, part, editorAutoSave] = await createEditorAutoSave({ autoSave: 'afterDelay', autoSaveDelay: 1 }); + const accessor = await createEditorAutoSave({ autoSave: 'afterDelay', autoSaveDelay: 1 }); const resource = toResource.call(this, '/path/index.txt'); @@ -88,16 +75,10 @@ suite('EditorAutoSave', () => { await awaitModelSaved(model); assert.ok(!model.isDirty()); - - part.dispose(); - editorAutoSave.dispose(); - (accessor.textFileService.files).dispose(); }); test.skip('editor auto saves on focus change if configured', async function () { // {{SQL CARBON EDIT}} skip failing test - this.retries(3); // https://github.com/microsoft/vscode/issues/108727 - - const [accessor, part, editorAutoSave] = await createEditorAutoSave({ autoSave: 'onFocusChange' }); + const accessor = await createEditorAutoSave({ autoSave: 'onFocusChange' }); const resource = toResource.call(this, '/path/index.txt'); await accessor.editorService.openEditor({ resource, forceFile: true }); @@ -112,15 +93,9 @@ suite('EditorAutoSave', () => { await awaitModelSaved(model); assert.ok(!model.isDirty()); - - part.dispose(); - editorAutoSave.dispose(); - (accessor.textFileService.files).dispose(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { - return new Promise(c => { - Event.once(model.onDidChangeDirty)(c); - }); + return Event.toPromise(Event.once(model.onDidChangeDirty)); } }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts index 2aa086f577..010653ef24 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileActions.test.ts @@ -258,7 +258,7 @@ suite('Files - Increment file name smart', () => { assert.strictEqual(result, '2-test.js'); }); - test('Increment file name with prefix version with `-` as separator', function () { + test('Increment file name with prefix version with `_` as separator', function () { const name = '1_test.js'; const result = incrementFileName(name, false, 'smart'); assert.strictEqual(result, '2_test.js'); @@ -270,6 +270,36 @@ suite('Files - Increment file name smart', () => { assert.strictEqual(result, '9007199254740992.test.1.js'); }); + test('Increment file name with just version and no extension', function () { + const name = '001004'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, '001005'); + }); + + test('Increment file name with just version and no extension, too big number', function () { + const name = '9007199254740992'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, '9007199254740992.1'); + }); + + test('Increment file name with no extension and no version', function () { + const name = 'file'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file1'); + }); + + test('Increment file name with no extension', function () { + const name = 'file1'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file2'); + }); + + test('Increment file name with no extension, too big number', function () { + const name = 'file9007199254740992'; + const result = incrementFileName(name, false, 'smart'); + assert.strictEqual(result, 'file9007199254740992.1'); + }); + test('Increment folder name with prefix version', function () { const name = '1.test'; const result = incrementFileName(name, true, 'smart'); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 43699f288d..75b0434a85 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -19,8 +19,10 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Registry } from 'vs/platform/registry/common/platform'; +import { FileEditorInputFactory } from 'vs/workbench/contrib/files/browser/files'; suite('Files - FileEditorInput', () => { + let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; @@ -102,28 +104,28 @@ suite('Files - FileEditorInput', () => { const preferredResource = toResource.call(this, '/foo/bar/UPDATEFILE.js'); const inputWithoutPreferredResource = createFileInput(resource); - assert.equal(inputWithoutPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); + assert.strictEqual(inputWithoutPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithoutPreferredResource.preferredResource.toString(), resource.toString()); const inputWithPreferredResource = createFileInput(resource, preferredResource); - assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), preferredResource.toString()); let didChangeLabel = false; const listener = inputWithPreferredResource.onDidChangeLabel(e => { didChangeLabel = true; }); - assert.equal(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); + assert.strictEqual(inputWithPreferredResource.getName(), 'UPDATEFILE.js'); const otherPreferredResource = toResource.call(this, '/FOO/BAR/updateFILE.js'); inputWithPreferredResource.setPreferredResource(otherPreferredResource); - assert.equal(inputWithPreferredResource.resource.toString(), resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); - assert.equal(inputWithPreferredResource.getName(), 'updateFILE.js'); - assert.equal(didChangeLabel, true); + assert.strictEqual(inputWithPreferredResource.resource.toString(), resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), otherPreferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.getName(), 'updateFILE.js'); + assert.strictEqual(didChangeLabel, true); listener.dispose(); }); @@ -135,20 +137,20 @@ suite('Files - FileEditorInput', () => { }); const input = createFileInput(toResource.call(this, '/foo/bar/file.js'), undefined, mode); - assert.equal(input.getPreferredMode(), mode); + assert.strictEqual(input.getPreferredMode(), mode); const model = await input.resolve() as TextFileEditorModel; - assert.equal(model.textEditorModel!.getModeId(), mode); + assert.strictEqual(model.textEditorModel!.getModeId(), mode); input.setMode('text'); - assert.equal(input.getPreferredMode(), 'text'); - assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); + assert.strictEqual(input.getPreferredMode(), 'text'); + assert.strictEqual(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); const input2 = createFileInput(toResource.call(this, '/foo/bar/file.js')); input2.setPreferredMode(mode); const model2 = await input2.resolve() as TextFileEditorModel; - assert.equal(model2.textEditorModel!.getModeId(), mode); + assert.strictEqual(model2.textEditorModel!.getModeId(), mode); }); test('matches', function () { @@ -169,10 +171,10 @@ suite('Files - FileEditorInput', () => { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); input.setEncoding('utf16', EncodingMode.Encode); - assert.equal(input.getEncoding(), 'utf16'); + assert.strictEqual(input.getEncoding(), 'utf16'); const resolved = await input.resolve() as TextFileEditorModel; - assert.equal(input.getEncoding(), resolved.getEncoding()); + assert.strictEqual(input.getEncoding(), resolved.getEncoding()); resolved.dispose(); }); @@ -237,7 +239,7 @@ suite('Files - FileEditorInput', () => { const model = await accessor.textFileService.files.resolve(input.resource); model.textEditorModel?.setValue('hello world'); - assert.equal(listenerCount, 1); + assert.strictEqual(listenerCount, 1); assert.ok(input.isDirty()); input.dispose(); @@ -264,12 +266,14 @@ suite('Files - FileEditorInput', () => { const input = createFileInput(toResource.call(this, '/foo/bar/updatefile.js')); + const disposable = Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory('workbench.editors.files.fileEditorInput', FileEditorInputFactory); + const factory = Registry.as(EditorExtensions.EditorInputFactories).getEditorInputFactory(input.getTypeId()); if (!factory) { assert.fail('File Editor Input Factory missing'); } - assert.equal(factory.canSerialize(input), true); + assert.strictEqual(factory.canSerialize(input), true); const inputSerialized = factory.serialize(input); if (!inputSerialized) { @@ -277,7 +281,7 @@ suite('Files - FileEditorInput', () => { } const inputDeserialized = factory.deserialize(instantiationService, inputSerialized); - assert.equal(input.matches(inputDeserialized), true); + assert.strictEqual(input.matches(inputDeserialized), true); const preferredResource = toResource.call(this, '/foo/bar/UPDATEfile.js'); const inputWithPreferredResource = createFileInput(toResource.call(this, '/foo/bar/updatefile.js'), preferredResource); @@ -288,8 +292,10 @@ suite('Files - FileEditorInput', () => { } const inputWithPreferredResourceDeserialized = factory.deserialize(instantiationService, inputWithPreferredResourceSerialized) as FileEditorInput; - assert.equal(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); - assert.equal(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); + assert.strictEqual(inputWithPreferredResource.resource.toString(), inputWithPreferredResourceDeserialized.resource.toString()); + assert.strictEqual(inputWithPreferredResource.preferredResource.toString(), inputWithPreferredResourceDeserialized.preferredResource.toString()); + + disposable.dispose(); }); test('preferred name/description', async function () { @@ -302,16 +308,16 @@ suite('Files - FileEditorInput', () => { didChangeLabelCounter++; }); - assert.equal(customFileInput.getName(), 'My Name'); - assert.equal(customFileInput.getDescription(), 'My Description'); + assert.strictEqual(customFileInput.getName(), 'My Name'); + assert.strictEqual(customFileInput.getDescription(), 'My Description'); customFileInput.setPreferredName('My Name 2'); customFileInput.setPreferredDescription('My Description 2'); - assert.equal(customFileInput.getName(), 'My Name 2'); - assert.equal(customFileInput.getDescription(), 'My Description 2'); + assert.strictEqual(customFileInput.getName(), 'My Name 2'); + assert.strictEqual(customFileInput.getDescription(), 'My Description 2'); - assert.equal(didChangeLabelCounter, 2); + assert.strictEqual(didChangeLabelCounter, 2); customFileInput.dispose(); @@ -323,16 +329,16 @@ suite('Files - FileEditorInput', () => { didChangeLabelCounter++; }); - assert.notEqual(fileInput.getName(), 'My Name'); - assert.notEqual(fileInput.getDescription(), 'My Description'); + assert.notStrictEqual(fileInput.getName(), 'My Name'); + assert.notStrictEqual(fileInput.getDescription(), 'My Description'); fileInput.setPreferredName('My Name 2'); fileInput.setPreferredDescription('My Description 2'); - assert.notEqual(fileInput.getName(), 'My Name 2'); - assert.notEqual(fileInput.getDescription(), 'My Description 2'); + assert.notStrictEqual(fileInput.getName(), 'My Name 2'); + assert.notStrictEqual(fileInput.getDescription(), 'My Description 2'); - assert.equal(didChangeLabelCounter, 0); + assert.strictEqual(didChangeLabelCounter, 0); fileInput.dispose(); }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 90832dee18..f44d9d508e 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -27,8 +27,8 @@ suite('Files - FileOnDiskContentProvider', () => { const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); assert.ok(content); - assert.equal(snapshotToString(content!.createSnapshot()), 'Hello Html'); - assert.equal(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); - assert.equal(accessor.fileService.getLastReadFileUri().path, uri.path); + assert.strictEqual(snapshotToString(content!.createSnapshot()), 'Hello Html'); + assert.strictEqual(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); + assert.strictEqual(accessor.fileService.getLastReadFileUri().path, uri.path); }); }); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts deleted file mode 100644 index 370996a79d..0000000000 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { toResource } from 'vs/base/test/common/utils'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService, TestTextResourceConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { Selection } from 'vs/editor/common/core/selection'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; - -suite('Files - TextFileEditor', () => { - - let disposables: IDisposable[] = []; - - setup(() => { - disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TextFileEditor, - TextFileEditor.ID, - 'Text File Editor' - ), - [new SyncDescriptor(FileEditorInput)] - )); - }); - - teardown(() => { - dispose(disposables); - disposables = []; - }); - - async function createPart(restoreViewState: boolean): Promise<[EditorPart, TestServiceAccessor, IInstantiationService, IEditorService]> { - const instantiationService = workbenchInstantiationService(); - - const configurationService = new TestConfigurationService(); - configurationService.setUserConfiguration('workbench', { editor: { restoreViewState } }); - instantiationService.stub(IConfigurationService, configurationService); - - instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configurationService)); - - instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( - instantiationService.createInstance(MockContextKeyService), - configurationService - )); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - instantiationService.stub(IEditorGroupsService, part); - - const editorService: EditorService = instantiationService.createInstance(EditorService); - instantiationService.stub(IEditorService, editorService); - - const accessor = instantiationService.createInstance(TestServiceAccessor); - - await part.whenRestored; - - return [part, accessor, instantiationService, editorService]; - } - - test.skip('text file editor preserves viewstate', async function () { // {{SQL CARBON EDIT}} skip failing test - return viewStateTest(this, true); - }); - - test.skip('text file editor resets viewstate if configured as such', async function () { // {{SQL CARBON EDIT}} skip failing test - return viewStateTest(this, false); - }); - - async function viewStateTest(context: Mocha.ITestCallbackContext, restoreViewState: boolean): Promise { - const [part, accessor] = await createPart(restoreViewState); - - let editor = await accessor.editorService.openEditor(accessor.editorService.createEditorInput({ resource: toResource.call(context, '/path/index.txt'), forceFile: true })); - - let codeEditor = editor?.getControl() as CodeEditorWidget; - const selection = new Selection(1, 3, 1, 4); - codeEditor.setSelection(selection); - - editor = await accessor.editorService.openEditor(accessor.editorService.createEditorInput({ resource: toResource.call(context, '/path/index-other.txt'), forceFile: true })); - editor = await accessor.editorService.openEditor(accessor.editorService.createEditorInput({ resource: toResource.call(context, '/path/index.txt'), forceFile: true })); - - codeEditor = editor?.getControl() as CodeEditorWidget; - - if (restoreViewState) { - assert.ok(codeEditor.getSelection()?.equalsSelection(selection)); - } else { - assert.ok(!codeEditor.getSelection()?.equalsSelection(selection)); - } - - part.dispose(); - (accessor.textFileService.files).dispose(); - } -}); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index 4d82878820..67c4818195 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -8,22 +8,15 @@ import { Event } from 'vs/base/common/event'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService, registerTestFileEditor, registerTestResourceEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IResolvedTextFileEditorModel, snapshotToString, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { timeout } from 'vs/base/common/async'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { EditorInput } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -35,25 +28,18 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; suite('Files - TextFileEditorTracker', () => { - let disposables: IDisposable[] = []; + const disposables = new DisposableStore(); setup(() => { - disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TextFileEditor, - TextFileEditor.ID, - 'Text File Editor' - ), - [new SyncDescriptor(FileEditorInput)] - )); + disposables.add(registerTestFileEditor()); + disposables.add(registerTestResourceEditor()); }); teardown(() => { - dispose(disposables); - disposables = []; + disposables.clear(); }); - async function createTracker(autoSaveEnabled = false): Promise<[EditorPart, TestServiceAccessor, TextFileEditorTracker, IInstantiationService, IEditorService]> { + async function createTracker(autoSaveEnabled = false): Promise { const instantiationService = workbenchInstantiationService(); if (autoSaveEnabled) { @@ -68,7 +54,7 @@ suite('Files - TextFileEditorTracker', () => { )); } - const part = instantiationService.createInstance(EditorPart); + const part = disposables.add(instantiationService.createInstance(EditorPart)); part.create(document.createElement('div')); part.layout(400, 300); @@ -78,23 +64,24 @@ suite('Files - TextFileEditorTracker', () => { instantiationService.stub(IEditorService, editorService); const accessor = instantiationService.createInstance(TestServiceAccessor); + disposables.add((accessor.textFileService.files)); await part.whenRestored; - const tracker = instantiationService.createInstance(TextFileEditorTracker); + disposables.add(instantiationService.createInstance(TextFileEditorTracker)); - return [part, accessor, tracker, instantiationService, editorService]; + return accessor; } test.skip('file change event updates model', async function () { // {{SQL CARBON EDIT}} tabcolormode failure - const [, accessor, tracker] = await createTracker(); + const accessor = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; model.textEditorModel.setValue('Super Good'); - assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); + assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Super Good'); await model.save(); @@ -103,10 +90,7 @@ suite('Files - TextFileEditorTracker', () => { await timeout(0); // due to event updating model async - assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); - - tracker.dispose(); - (accessor.textFileService.files).dispose(); + assert.strictEqual(snapshotToString(model.createSnapshot()!), 'Hello Html'); }); test.skip('dirty text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure @@ -122,7 +106,7 @@ suite('Files - TextFileEditorTracker', () => { }); async function testDirtyTextFileModelOpensEditorDependingOnAutoSaveSetting(resource: URI, autoSave: boolean): Promise { - const [part, accessor, tracker] = await createTracker(autoSave); + const accessor = await createTracker(autoSave); assert.ok(!accessor.editorService.isOpen(accessor.editorService.createEditorInput({ resource, forceFile: true }))); @@ -137,17 +121,13 @@ suite('Files - TextFileEditorTracker', () => { await awaitEditorOpening(accessor.editorService); assert.ok(accessor.editorService.isOpen(accessor.editorService.createEditorInput({ resource, forceFile: true }))); } - - part.dispose(); - tracker.dispose(); - (accessor.textFileService.files).dispose(); } test.skip('dirty untitled text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure - const [part, accessor, tracker, , editorService] = await createTracker(); + const accessor = await createTracker(); - const untitledEditor = editorService.createEditorInput({ forceUntitled: true }) as UntitledTextEditorInput; - const model = await untitledEditor.resolve(); + const untitledEditor = accessor.editorService.createEditorInput({ forceUntitled: true }) as UntitledTextEditorInput; + const model = disposables.add(await untitledEditor.resolve()); assert.ok(!accessor.editorService.isOpen(untitledEditor)); @@ -155,20 +135,14 @@ suite('Files - TextFileEditorTracker', () => { await awaitEditorOpening(accessor.editorService); assert.ok(accessor.editorService.isOpen(untitledEditor)); - - part.dispose(); - tracker.dispose(); - model.dispose(); }); function awaitEditorOpening(editorService: IEditorService): Promise { - return new Promise(c => { - Event.once(editorService.onDidActiveEditorChange)(c); - }); + return Event.toPromise(Event.once(editorService.onDidActiveEditorChange)); } test.skip('non-dirty files reload on window focus', async function () { // {{SQL CARBON EDIT}} skip failing test - const [part, accessor, tracker] = await createTracker(); + const accessor = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -178,10 +152,6 @@ suite('Files - TextFileEditorTracker', () => { accessor.hostService.setFocus(true); await awaitModelLoadEvent(accessor.textFileService, resource); - - part.dispose(); - tracker.dispose(); - (accessor.textFileService.files).dispose(); }); function awaitModelLoadEvent(textFileService: ITextFileService, resource: URI): Promise { diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 808731151f..5dc8a6029f 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -30,6 +30,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; import { mergeSort } from 'vs/base/common/arrays'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -45,6 +46,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @IConfigurationService private readonly _configService: IConfigurationService, @INotificationService private readonly _notificationService: INotificationService, + @IDialogService private readonly _dialogService: IDialogService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IModeService private readonly _modeService: IModeService, @ILabelService private readonly _labelService: ILabelService, @@ -119,25 +121,32 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { } const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); - const silent = mode === FormattingMode.Silent; const message = !defaultFormatterId ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)) : nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId); - return new Promise((resolve, reject) => { + if (mode !== FormattingMode.Silent) { + // running from a user action -> show modal dialog so that users configure + // a default formatter + const result = await this._dialogService.confirm({ + message, + primaryButton: nls.localize('do.config', "Configure..."), + secondaryButton: nls.localize('cancel', "Cancel") + }); + if (result.confirmed) { + return this._pickAndPersistDefaultFormatter(formatter, document); + } + + } else { + // no user action -> show a silent notification and proceed this._notificationService.prompt( Severity.Info, message, - [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document).then(resolve, reject) }], - { silent, onCancel: () => resolve(undefined) } + [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document) }], + { silent: true } ); - - if (silent) { - // don't wait when formatting happens without interaction - // but pick some formatter... - resolve(undefined); - } - }); + } + return undefined; } private async _pickAndPersistDefaultFormatter(formatter: T[], document: ITextModel): Promise { diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index bc266281d8..2b4b0d1eeb 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -6,7 +6,7 @@ import { IssueReporterStyles, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; -import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -17,6 +17,7 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; export class WorkbenchIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; @@ -28,7 +29,8 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @ITASExperimentService private readonly experimentService: ITASExperimentService + @ITASExperimentService private readonly experimentService: ITASExperimentService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService ) { } async openReporter(dataOverrides: Partial = {}): Promise { @@ -52,12 +54,15 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { }; }); const experiments = await this.experimentService.getCurrentExperiments(); + const githubSessions = await this.authenticationService.getSessions('github'); + const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo')); const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = Object.assign({ styles: getIssueReporterStyles(theme), zoomLevel: getZoomLevel(), enabledExtensions: extensionData, - experiments: experiments?.join('\n') + experiments: experiments?.join('\n'), + githubAccessToken: potentialSessions[0]?.accessToken }, dataOverrides); return this.issueService.openReporter(issueReporterData); } @@ -71,8 +76,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { backgroundColor: getColor(theme, editorBackground), color: getColor(theme, editorForeground), hoverBackground: getColor(theme, listHoverBackground), - hoverForeground: getColor(theme, listHoverForeground), - highlightForeground: getColor(theme, listHighlightForeground), + hoverForeground: getColor(theme, listHoverForeground) }, platform: process.platform, applicationName: this.productService.applicationName diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts index 4ad33f996c..6e4bac356b 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts @@ -61,7 +61,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo updateAndRestart ? localize('updateLocale', "Would you like to change Azure Data Studio's UI language to {0} and restart?", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId) : localize('activateLanguagePack', "In order to use Azure Data Studio in {0}, Azure Data Studio needs to restart.", e.local.manifest.contributes.localizations[0].languageName || e.local.manifest.contributes.localizations[0].languageId), [{ - label: updateAndRestart ? localize('yes', "Yes") : localize('restart now', "Restart Now"), + label: updateAndRestart ? localize('changeAndRestart', "Change Language and Restart") : localize('restart', "Restart"), run: () => { const updatePromise = updateAndRestart ? this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true) : Promise.resolve(undefined); updatePromise.then(() => this.hostService.restart(), e => this.notificationService.error(e)); diff --git a/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts index b8356a7045..7b87a394d3 100644 --- a/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts +++ b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts @@ -9,6 +9,7 @@ import { basename, dirname } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { URI } from 'vs/base/common/uri'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { Promises } from 'vs/base/common/async'; export class LogsDataCleaner extends Disposable { @@ -31,7 +32,7 @@ export class LogsDataCleaner extends Disposable { const allSessions = stat.children.filter(stat => stat.isDirectory && /^\d{8}T\d{6}$/.test(stat.name)); const oldSessions = allSessions.sort().filter((d, i) => d.name !== currentLog); const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 49)); - Promise.all(toDelete.map(stat => this.fileService.del(stat.resource, { recursive: true }))); + Promises.settled(toDelete.map(stat => this.fileService.del(stat.resource, { recursive: true }))); } }, 10 * 1000); this.lifecycleService.onWillShutdown(() => { diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 052d32bef6..537b92abc6 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -4,35 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/workbench/contrib/markers/browser/markersFileDecorations'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { CATEGORIES } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; import { Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; import { MarkersView } from 'vs/workbench/contrib/markers/browser/markersView'; -import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ShowProblemsPanelAction } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ActivityUpdater } from 'vs/workbench/contrib/markers/browser/markers'; +import { ActivityUpdater, IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext, IViewDescriptorService } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ToggleViewAction } from 'vs/workbench/browser/actions/layoutActions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.MARKER_OPEN_ACTION_ID, @@ -107,38 +104,18 @@ Registry.as(Extensions.Configuration).registerConfigurat } }); -class ToggleMarkersPanelAction extends ToggleViewAction { - - public static readonly ID = 'workbench.actions.view.problems'; - public static readonly LABEL = Messages.MARKERS_PANEL_TOGGLE_LABEL; - - constructor(id: string, label: string, - @IViewsService viewsService: IViewsService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService contextKeyService: IContextKeyService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService - ) { - super(id, label, Constants.MARKERS_VIEW_ID, viewsService, viewDescriptorService, contextKeyService, layoutService); - } -} - const markersViewIcon = registerIcon('markers-view-icon', Codicon.warning, localize('markersViewIcon', 'View icon of the markers view.')); // markers view container const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Constants.MARKERS_CONTAINER_ID, - name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, + title: Messages.MARKERS_PANEL_TITLE_PROBLEMS, icon: markersViewIcon, hideIfEmpty: true, order: 0, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: Constants.MARKERS_VIEW_STORAGE_ID, - focusCommand: { - id: ToggleMarkersPanelAction.ID, keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M - } - } -}, ViewContainerLocation.Panel); +}, ViewContainerLocation.Panel, { donotRegisterOpenCommand: true }); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ id: Constants.MARKERS_VIEW_ID, @@ -147,6 +124,12 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(MarkersView), + openCommandActionDescriptor: { + id: 'workbench.actions.view.problems', + mnemonicTitle: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems"), + keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M }, + order: 0, + } }], VIEW_CONTAINER); // workbench @@ -154,12 +137,21 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ActivityUpdater, LifecyclePhase.Restored); // actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMarkersPanelAction, { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M -}), 'View: Toggle Problems (Errors, Warnings, Infos)', CATEGORIES.View.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowProblemsPanelAction), 'View: Focus Problems (Errors, Warnings, Infos)', CATEGORIES.View.value); registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.problems.focus', + title: { value: Messages.MARKERS_PANEL_SHOW_LABEL, original: 'Focus Problems (Errors, Warnings, Infos)' }, + category: CATEGORIES.View.value, + f1: true, + }); + } + async run(accessor: ServicesAccessor): Promise { + accessor.get(IViewsService).openView(Constants.MARKERS_VIEW_ID, true); + } +}); + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKER_COPY_ACTION_ID, @@ -174,13 +166,19 @@ registerAction2(class extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, when: Constants.MarkerFocusContextKey }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyMarker(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof Marker) { + await clipboardService.writeText(`${element}`); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, @@ -190,13 +188,19 @@ registerAction2(class extends Action2 { when: Constants.MarkerFocusContextKey, group: 'navigation' }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyMessage(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof Marker) { + await clipboardService.writeText(element.marker.message); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, @@ -205,14 +209,20 @@ registerAction2(class extends Action2 { id: MenuId.ProblemsPanelContext, when: Constants.RelatedInformationFocusContextKey, group: 'navigation' - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - async run(accessor: ServicesAccessor) { - await copyRelatedInformationMessage(accessor.get(IViewsService), accessor.get(IClipboardService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + const clipboardService = serviceAccessor.get(IClipboardService); + const element = markersView.getFocusElement(); + if (element instanceof RelatedInformation) { + await clipboardService.writeText(element.raw.message); + } } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.FOCUS_PROBLEMS_FROM_FILTER, @@ -221,14 +231,16 @@ registerAction2(class extends Action2 { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.DownArrow - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - focusProblemsView(accessor.get(IViewsService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.focus(); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_FOCUS_FILTER, @@ -237,14 +249,16 @@ registerAction2(class extends Action2 { when: FocusedViewContext.isEqualTo(Constants.MARKERS_VIEW_ID), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_F - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - focusProblemsFilter(accessor.get(IViewsService)); + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.focusFilter(); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_SHOW_MULTILINE_MESSAGE, @@ -253,17 +267,16 @@ registerAction2(class extends Action2 { menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID)!; - if (markersView) { - markersView.markersViewModel.multiline = true; - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.setMultiline(true); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE, @@ -272,17 +285,16 @@ registerAction2(class extends Action2 { menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.markersViewModel.multiline = false; - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.setMultiline(false); } }); -registerAction2(class extends Action2 { + +registerAction2(class extends ViewAction { constructor() { super({ id: Constants.MARKERS_VIEW_CLEAR_FILTER_TEXT, @@ -291,76 +303,65 @@ registerAction2(class extends Action2 { keybinding: { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, - } + }, + viewId: Constants.MARKERS_VIEW_ID }); } - run(accessor: ServicesAccessor) { - const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.clearFilterText(); - } + async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise { + markersView.clearFilterText(); } }); -async function copyMarker(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof Marker) { - await clipboardService.writeText(`${element}`); - } +registerAction2(class extends ViewAction { + constructor() { + super({ + id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', Constants.MARKERS_VIEW_ID), + group: 'navigation', + order: 2, + }, + icon: Codicon.collapseAll, + viewId: Constants.MARKERS_VIEW_ID + }); } -} - -async function copyMessage(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof Marker) { - await clipboardService.writeText(element.marker.message); - } + async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise { + return view.collapseAll(); } -} - -async function copyRelatedInformationMessage(viewsService: IViewsService, clipboardService: IClipboardService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - const element = markersView.getFocusElement(); - if (element instanceof RelatedInformation) { - await clipboardService.writeText(element.raw.message); - } - } -} - -function focusProblemsView(viewsService: IViewsService) { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.focus(); - } -} - -function focusProblemsFilter(viewsService: IViewsService): void { - const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); - if (markersView) { - markersView.focusFilter(); - } -} - -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '4_panels', - command: { - id: ToggleMarkersPanelAction.ID, - title: localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") - }, - order: 4 }); -CommandsRegistry.registerCommand(Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, async (accessor) => { - const viewsService = accessor.get(IViewsService); - if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { - viewsService.closeView(Constants.MARKERS_VIEW_ID); - } else { - viewsService.openView(Constants.MARKERS_VIEW_ID, true); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.filter`, + title: localize('filter', "Filter"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', Constants.MARKERS_VIEW_ID), Constants.MarkersViewSmallLayoutContextKey.negate()), + group: 'navigation', + order: 1, + }, + }); + } + async run(): Promise { } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, + title: Messages.MARKERS_PANEL_TOGGLE_LABEL, + }); + } + async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { + viewsService.closeView(Constants.MARKERS_VIEW_ID); + } else { + viewsService.openView(Constants.MARKERS_VIEW_ID, true); + } } }); diff --git a/src/vs/workbench/contrib/markers/browser/markers.ts b/src/vs/workbench/contrib/markers/browser/markers.ts index b3b9addd03..4ea4e2f323 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.ts @@ -9,6 +9,26 @@ import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/co import { localize } from 'vs/nls'; import Constants from './constants'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { MarkersFilters } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { Event } from 'vs/base/common/event'; +import { IView } from 'vs/workbench/common/views'; +import { MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; + +export interface IMarkersView extends IView { + + readonly onDidFocusFilter: Event; + readonly onDidClearFilterText: Event; + readonly filters: MarkersFilters; + readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; + focusFilter(): void; + clearFilterText(): void; + getFilterStats(): { total: number, filtered: number }; + + getFocusElement(): MarkerElement | undefined; + + collapseAll(): void; + setMultiline(multiline: boolean): void; +} export class ActivityUpdater extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/markers/browser/markersModel.ts b/src/vs/workbench/contrib/markers/browser/markersModel.ts index a46ccc096c..d84a8ceb10 100644 --- a/src/vs/workbench/contrib/markers/browser/markersModel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersModel.ts @@ -14,6 +14,7 @@ import { Hasher } from 'vs/base/common/hash'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { splitLines } from 'vs/base/common/strings'; +export type MarkerElement = ResourceMarkers | Marker | RelatedInformation; export function compareMarkersByUri(a: IMarker, b: IMarker) { return extUri.compare(a.resource, b.resource); diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 7211a97bbf..07cc1494ff 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -10,7 +10,7 @@ import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { ResourceMarkers, Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { ResourceMarkers, Marker, RelatedInformation, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; @@ -56,8 +56,6 @@ import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -export type TreeElement = ResourceMarkers | Marker | RelatedInformation; - interface IResourceMarkersTemplateData { resourceLabel: IResourceLabel; count: CountBadge; @@ -74,7 +72,7 @@ interface IRelatedInformationTemplateData { description: HighlightedLabel; } -export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider { +export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider { constructor(@ILabelService private readonly labelService: ILabelService) { } @@ -82,7 +80,7 @@ export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvi return localize('problemsView', "Problems View"); } - public getAriaLabel(element: TreeElement): string | null { + public getAriaLabel(element: MarkerElement): string | null { if (element instanceof ResourceMarkers) { const path = this.labelService.getUriLabel(element.resource, { relative: true }) || element.resource.fsPath; return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.markers.length, element.name, paths.dirname(path)); @@ -103,13 +101,13 @@ const enum TemplateId { RelatedInformation = 'ri' } -export class VirtualDelegate implements IListVirtualDelegate { +export class VirtualDelegate implements IListVirtualDelegate { static LINE_HEIGHT: number = 22; constructor(private readonly markersViewState: MarkersViewModel) { } - getHeight(element: TreeElement): number { + getHeight(element: MarkerElement): number { if (element instanceof Marker) { const viewModel = this.markersViewState.getViewModel(element); const noOfLines = !viewModel || viewModel.multiline ? element.lines.length : 1; @@ -118,7 +116,7 @@ export class VirtualDelegate implements IListVirtualDelegate { return VirtualDelegate.LINE_HEIGHT; } - getTemplateId(element: TreeElement): string { + getTemplateId(element: MarkerElement): string { if (element instanceof ResourceMarkers) { return TemplateId.ResourceMarkers; } else if (element instanceof Marker) { @@ -512,11 +510,11 @@ export class RelatedInformationRenderer implements ITreeRenderer { +export class Filter implements ITreeFilter { constructor(public options: FilterOptions) { } - filter(element: TreeElement, parentVisibility: TreeVisibility): TreeFilterResult { + filter(element: MarkerElement, parentVisibility: TreeVisibility): TreeFilterResult { if (element instanceof ResourceMarkers) { return this.filterResourceMarkers(element); } else if (element instanceof Marker) { @@ -852,23 +850,23 @@ export class MarkersViewModel extends Disposable { } -export class ResourceDragAndDrop implements ITreeDragAndDrop { +export class ResourceDragAndDrop implements ITreeDragAndDrop { constructor( private instantiationService: IInstantiationService ) { } - onDragOver(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + onDragOver(data: IDragAndDropData, targetElement: MarkerElement, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { return false; } - getDragURI(element: TreeElement): string | null { + getDragURI(element: MarkerElement): string | null { if (element instanceof ResourceMarkers) { return element.resource.toString(); } return null; } - getDragLabel?(elements: TreeElement[]): string | undefined { + getDragLabel?(elements: MarkerElement[]): string | undefined { if (elements.length > 1) { return String(elements.length); } @@ -877,7 +875,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { } onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { - const elements = (data as ElementsDragAndDropData).elements; + const elements = (data as ElementsDragAndDropData).elements; const resources: URI[] = elements .filter(e => e instanceof ResourceMarkers) .map(resourceMarker => (resourceMarker as ResourceMarkers).resource); @@ -888,7 +886,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { } } - drop(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): void { + drop(data: IDragAndDropData, targetElement: MarkerElement, targetIndex: number, originalEvent: DragEvent): void { } } diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index c72b07782f..5d22e3afa4 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -11,17 +11,17 @@ import { IAction, IActionViewItem, Action, Separator } from 'vs/base/common/acti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; -import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; -import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; +import { RangeHighlightDecorations } from 'vs/workbench/browser/codeeditor'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { localize } from 'vs/nls'; -import { IContextKey, IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Iterable } from 'vs/base/common/iterator'; import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Relay, Event, Emitter } from 'vs/base/common/event'; @@ -30,10 +30,10 @@ import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilte import { IExpression } from 'vs/base/common/glob'; import { deepClone } from 'vs/base/common/objects'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; +import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { domEvent } from 'vs/base/browser/event'; @@ -45,7 +45,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Codicon } from 'vs/base/common/codicons'; @@ -54,8 +54,10 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { groupBy } from 'vs/base/common/arrays'; import { ResourceMap } from 'vs/base/common/map'; +import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; -function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { +function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { const relatedInformationIt = Iterable.from(m.relatedInformation); const children = Iterable.map(relatedInformationIt, r => ({ element: r })); @@ -64,7 +66,7 @@ function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterab }); } -export class MarkersView extends ViewPane implements IMarkerFilterController { +export class MarkersView extends ViewPane implements IMarkersView { private lastSelectedRelativeTop: number = 0; private currentActiveResource: URI | null = null; @@ -87,7 +89,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private cachedFilterStats: { total: number; filtered: number; } | undefined = undefined; private currentResourceGotAddedToMarkersData: boolean = false; - readonly markersViewModel: MarkersViewModel; + private readonly markersViewModel: MarkersViewModel; private readonly smallLayoutContextKey: IContextKey; private get smallLayout(): boolean { return !!this.smallLayoutContextKey.get(); } private set smallLayout(smallLayout: boolean) { this.smallLayoutContextKey.set(smallLayout); } @@ -131,8 +133,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.filter = new Filter(FilterOptions.EMPTY(uriIdentityService)); this.rangeHighlightDecorations = this._register(this.instantiationService.createInstance(RangeHighlightDecorations)); - // actions - this.regiserActions(); this.filters = this._register(new MarkersFilters({ filterText: this.panelState['filter'] || '', filterHistory: this.panelState['filterHistory'] || [], @@ -207,43 +207,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._onDidClearFilterText.fire(); } - private regiserActions(): void { - const that = this; - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.collapseAll`, - title: localize('collapseAll', "Collapse All"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyEqualsExpr.create('view', that.id), - group: 'navigation', - order: Number.MAX_SAFE_INTEGER, - }, - icon: Codicon.collapseAll - }); - } - async run(): Promise { - return that.collapseAll(); - } - })); - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: `workbench.actions.treeView.${that.id}.filter`, - title: localize('filter', "Filter"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), Constants.MarkersViewSmallLayoutContextKey.negate()), - group: 'navigation', - order: 1, - }, - }); - } - async run(): Promise { } - })); - } - public showQuickFixes(marker: Marker): void { const viewModel = this.markersViewModel.getViewModel(marker); if (viewModel) { @@ -327,11 +290,15 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } private setTreeSelection(): void { - if (this.tree && this.tree.getSelection().length === 0) { - const firstMarker = this.markersModel.resourceMarkers[0]?.markers[0]; - if (firstMarker) { - this.tree.setFocus([firstMarker]); - this.tree.setSelection([firstMarker]); + if (this.tree && this.tree.isVisible() && this.tree.getSelection().length === 0) { + const firstVisibleElement = this.tree.firstVisibleElement; + const marker = firstVisibleElement ? + firstVisibleElement instanceof ResourceMarkers ? firstVisibleElement.markers[0] : + firstVisibleElement instanceof Marker ? firstVisibleElement : undefined + : undefined; + if (marker) { + this.tree.setFocus([marker]); + this.tree.setSelection([marker]); } } } @@ -418,7 +385,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { const accessibilityProvider = this.instantiationService.createInstance(MarkersTreeAccessibilityProvider); const identityProvider = { - getId(element: TreeElement) { + getId(element: MarkerElement) { return element.id; } }; @@ -433,7 +400,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { accessibilityProvider, identityProvider, dnd: new ResourceDragAndDrop(this.instantiationService), - expandOnlyOnTwistieClick: (e: TreeElement) => e instanceof Marker && e.relatedInformation.length > 0, + expandOnlyOnTwistieClick: (e: MarkerElement) => e instanceof Marker && e.relatedInformation.length > 0, overrideStyles: { listBackground: this.getBackgroundColor() }, @@ -496,7 +463,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._register(this.tree.onDidChangeSelection(() => this.onSelected())); } - private collapseAll(): void { + collapseAll(): void { if (this.tree) { this.tree.collapseAll(); this.tree.setSelection([]); @@ -506,6 +473,10 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } } + setMultiline(multiline: boolean): void { + this.markersViewModel.multiline = multiline; + } + private onDidChangeMarkersViewVisibility(visible: boolean): void { this.onVisibleDisposables.clear(); if (visible) { @@ -603,7 +574,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private setCurrentActiveEditor(): void { const activeEditor = this.editorService.activeEditor; - this.currentActiveResource = activeEditor ? withUndefinedAsNull(activeEditor.resource) : null; + this.currentActiveResource = activeEditor ? withUndefinedAsNull(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY })) : null; } private onSelected(): void { @@ -785,7 +756,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.rangeHighlightDecorations.highlightRange(selection); } - private onContextMenu(e: ITreeContextMenuEvent): void { + private onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; if (!element) { return; @@ -812,7 +783,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { }); } - private getMenuActions(element: TreeElement): IAction[] { + private getMenuActions(element: MarkerElement): IAction[] { const result: IAction[] = []; if (element instanceof Marker) { @@ -840,8 +811,8 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { return result; } - public getFocusElement() { - return this.tree ? this.tree.getFocus()[0] : undefined; + public getFocusElement(): MarkerElement | undefined { + return this.tree?.getFocus()[0] || undefined; } public getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -919,14 +890,14 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } -class MarkersTree extends WorkbenchObjectTree { +class MarkersTree extends WorkbenchObjectTree { constructor( user: string, readonly container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], - options: IWorkbenchObjectTreeOptions, + delegate: IListVirtualDelegate, + renderers: ITreeRenderer[], + options: IWorkbenchObjectTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, @@ -946,6 +917,10 @@ class MarkersTree extends WorkbenchObjectTree { this.container.classList.toggle('hidden', hide); } + isVisible(): boolean { + return !this.container.classList.contains('hidden'); + } + } registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 2102bd93cc..ab9f869989 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -24,27 +24,11 @@ import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { IViewsService } from 'vs/workbench/common/views'; import { Codicon } from 'vs/base/common/codicons'; import { BaseActionViewItem, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -export class ShowProblemsPanelAction extends Action { - - public static readonly ID = 'workbench.action.problems.focus'; - public static readonly LABEL = Messages.MARKERS_PANEL_SHOW_LABEL; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - public run(): Promise { - return this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); - } -} +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; export interface IMarkersFiltersChangeEvent { filterText?: boolean; @@ -164,14 +148,6 @@ export class MarkersFilters extends Disposable { } } -export interface IMarkerFilterController { - readonly onDidFocusFilter: Event; - readonly onDidClearFilterText: Event; - readonly filters: MarkersFilters; - readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; - getFilterStats(): { total: number, filtered: number }; -} - class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { constructor( @@ -276,7 +252,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { constructor( action: IAction, - private filterController: IMarkerFilterController, + private markersView: IMarkersView, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, @@ -286,11 +262,11 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { this.focusContextKey = Constants.MarkerViewFilterFocusContextKey.bindTo(contextKeyService); this.delayedFilterUpdate = new Delayer(400); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); - this._register(filterController.onDidFocusFilter(() => this.focus())); - this._register(filterController.onDidClearFilterText(() => this.clearFilterText())); + this._register(markersView.onDidFocusFilter(() => this.focus())); + this._register(markersView.onDidClearFilterText(() => this.clearFilterText())); this.filtersAction = new Action('markersFiltersAction', Messages.MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS, 'markers-filters ' + ThemeIcon.asClassName(filterIcon)); this.filtersAction.checked = this.hasFiltersChanged(); - this._register(filterController.filters.onDidChange(e => this.onDidFiltersChange(e))); + this._register(markersView.filters.onDidChange(e => this.onDidFiltersChange(e))); } render(container: HTMLElement): void { @@ -326,21 +302,21 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } private hasFiltersChanged(): boolean { - return !this.filterController.filters.showErrors || !this.filterController.filters.showWarnings || !this.filterController.filters.showInfos || this.filterController.filters.excludedFiles || this.filterController.filters.activeFile; + return !this.markersView.filters.showErrors || !this.markersView.filters.showWarnings || !this.markersView.filters.showInfos || this.markersView.filters.excludedFiles || this.markersView.filters.activeFile; } private createInput(container: HTMLElement): void { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, - history: this.filterController.filters.filterHistory + history: this.markersView.filters.filterHistory })); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); - this.filterInputBox.value = this.filterController.filters.filterText; + this.filterInputBox.value = this.markersView.filters.filterText; this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); - this._register(this.filterController.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { + this._register(this.markersView.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { if (event.filterText) { - this.filterInputBox!.value = this.filterController.filters.filterText; + this.filterInputBox!.value = this.markersView.filters.filterText; } })); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!))); @@ -378,14 +354,14 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { filterBadge.style.color = foreground; })); this.updateBadge(); - this._register(this.filterController.onDidChangeFilterStats(() => this.updateBadge())); + this._register(this.markersView.onDidChangeFilterStats(() => this.updateBadge())); } private createFilters(container: HTMLElement): void { const actionbar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === this.filtersAction.id) { - return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.filterController.filters, this.actionRunner); + return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.markersView.filters, this.actionRunner); } return undefined; } @@ -395,13 +371,13 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private onDidInputChange(inputbox: HistoryInputBox) { inputbox.addToHistory(); - this.filterController.filters.filterText = inputbox.value; - this.filterController.filters.filterHistory = inputbox.getHistory(); + this.markersView.filters.filterText = inputbox.value; + this.markersView.filters.filterHistory = inputbox.getHistory(); } private updateBadge(): void { if (this.filterBadge) { - const { total, filtered } = this.filterController.getFilterStats(); + const { total, filtered } = this.markersView.getFilterStats(); this.filterBadge.classList.toggle('hidden', total === filtered || total === 0); this.filterBadge.textContent = localize('showing filtered problems', "Showing {0} of {1}", filtered, total); this.adjustInputBox(); @@ -446,9 +422,9 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } protected get class(): string { - if (this.filterController.filters.layout.width > 600) { + if (this.markersView.filters.layout.width > 600) { return 'markers-panel-action-filter grow'; - } else if (this.filterController.filters.layout.width < 400) { + } else if (this.markersView.filters.layout.width < 400) { return 'markers-panel-action-filter small'; } else { return 'markers-panel-action-filter'; diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 14c00e8f3f..dcdba43acf 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -19,8 +19,8 @@ export default class Messages { public static MARKERS_PANEL_TITLE_PROBLEMS: string = nls.localize('markers.panel.title.problems', "Problems"); - public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace so far."); - public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file so far."); + public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace."); + public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file."); public static MARKERS_PANEL_NO_PROBLEMS_FILTERS: string = nls.localize('markers.panel.no.problems.filters', "No results found with provided filter criteria."); public static MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS: string = nls.localize('markers.panel.action.moreFilters', "More Filters..."); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index 14db186a46..9eb997a444 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,8 +13,8 @@ export const CELL_RUN_GUTTER = 28; export const CODE_CELL_LEFT_MARGIN = 32; export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_GAP = 18; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 50; +export const BOTTOM_CELL_TOOLBAR_GAP = 16; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 24; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 4be68a48de..acb56f96ee 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -23,11 +23,17 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ 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 { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellEditType, CellKind, ICellEditOperation, ICellRange, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BEGIN_END, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +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'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -137,14 +143,17 @@ abstract class NotebookAction extends Action2 { super(desc); } - async run(accessor: ServicesAccessor, context?: INotebookActionContext): Promise { + async run(accessor: ServicesAccessor, context?: any): Promise { if (!this.isNotebookActionContext(context)) { - context = this.getActiveEditorContext(accessor); + context = this.getEditorContextFromArgsOrActive(accessor, context); if (!context) { return; } } + const telemetryService = accessor.get(ITelemetryService); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: 'ui' }); + this.runWithContext(accessor, context); } @@ -154,7 +163,7 @@ abstract class NotebookAction extends Action2 { return !!context && !!(context as INotebookActionContext).notebookEditor; } - protected getActiveEditorContext(accessor: ServicesAccessor): INotebookActionContext | undefined { + protected getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: any): INotebookActionContext | undefined { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -180,22 +189,22 @@ abstract class NotebookCellAction extends Notebo return !!context && !!(context as INotebookCellActionContext).notebookEditor && !!(context as INotebookCellActionContext).cell; } - protected getCellContextFromArgs(accessor: ServicesAccessor, context?: T): INotebookCellActionContext | undefined { + protected getCellContextFromArgs(accessor: ServicesAccessor, context?: T, ...additionalArgs: any[]): INotebookCellActionContext | undefined { return undefined; } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext, ...additionalArgs: any[]): Promise { if (this.isCellActionContext(context)) { return this.runWithContext(accessor, context); } - const contextFromArgs = this.getCellContextFromArgs(accessor, context); + const contextFromArgs = this.getCellContextFromArgs(accessor, context, ...additionalArgs); if (contextFromArgs) { return this.runWithContext(accessor, contextFromArgs); } - const activeEditorContext = this.getActiveEditorContext(accessor); + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (this.isCellActionContext(activeEditorContext)) { return this.runWithContext(accessor, activeEditorContext); } @@ -204,6 +213,22 @@ abstract class NotebookCellAction extends Notebo abstract runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } +export function getWidgetFromUri(accessor: ServicesAccessor, uri: URI) { + const editorService = accessor.get(IEditorService); + const notebookWidgetService = accessor.get(INotebookEditorWidgetService); + const editorId = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editorId => editorId.editor instanceof NotebookEditorInput && editorId.editor.resource?.toString() === uri.toString()); + + if (editorId) { + const widget = notebookWidgetService.widgets.find(widget => widget.textModel?.viewType === (editorId.editor as NotebookEditorInput).viewType && widget.uri?.toString() === editorId.editor.resource!.toString()); + + if (widget && widget.hasModel()) { + return widget; + } + } + + return undefined; +} + registerAction2(class extends NotebookCellAction { constructor() { super({ @@ -235,6 +260,11 @@ registerAction2(class extends NotebookCellAction { } } } + }, + { + name: 'uri', + description: 'The document uri', + constraint: URI } ] }, @@ -242,12 +272,28 @@ registerAction2(class extends NotebookCellAction { }); } - getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange): INotebookCellActionContext | undefined { + getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { return undefined; // {{SQL CARBON EDIT}} } - const activeEditorContext = this.getActiveEditorContext(accessor); + if (additionalArgs.length && additionalArgs[0]) { + const uri = URI.revive(additionalArgs[0]); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + if (widget) { + const cells = widget.viewModel.viewCells; + + return { + notebookEditor: widget, + cell: cells[context.start] + }; + } + } + } + + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.viewCells.length) { return undefined; // {{SQL CARBON EDIT}} @@ -274,7 +320,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.cancel', "Stop Cell Execution"), icon: icons.stopIcon, description: { - description: localize('notebookActions.execute', "Execute Cell"), + description: localize('notebookActions.cancel', "Stop Cell Execution"), args: [ { name: 'range', @@ -291,18 +337,39 @@ registerAction2(class extends NotebookCellAction { } } } + }, + { + name: 'uri', + description: 'The document uri', + constraint: URI } ] }, }); } - getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange): INotebookCellActionContext | undefined { + getCellContextFromArgs(accessor: ServicesAccessor, context?: ICellRange, ...additionalArgs: any[]): INotebookCellActionContext | undefined { if (!context || typeof context.start !== 'number' || typeof context.end !== 'number' || context.start >= context.end) { return undefined; // {{SQL CARBON EDIT}} } - const activeEditorContext = this.getActiveEditorContext(accessor); + if (additionalArgs.length && additionalArgs[0]) { + const uri = URI.revive(additionalArgs[0]); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + if (widget) { + const cells = widget.viewModel.viewCells; + + return { + notebookEditor: widget, + cell: cells[context.start] + }; + } + } + } + + const activeEditorContext = this.getEditorContextFromArgsOrActive(accessor); if (!activeEditorContext || !activeEditorContext.notebookEditor.viewModel || context.start >= activeEditorContext.notebookEditor.viewModel.viewCells.length) { return undefined; // {{SQL CARBON EDIT}} @@ -456,19 +523,62 @@ registerAction2(class extends NotebookAction { super({ id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.executeNotebook', "Execute Notebook"), + description: { + description: localize('notebookActions.executeNotebook', "Execute Notebook"), + args: [ + { + name: 'uri', + description: 'The document uri', + constraint: URI + } + ] + }, }); } + getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + if (context) { + const uri = URI.revive(context); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + + if (widget) { + return { + notebookEditor: widget, + }; + } + } + } + + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return undefined; + } + + if (!editor.hasModel()) { + return undefined; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { renderAllMarkdownCells(context); + const editorService = accessor.get(IEditorService); + const editor = editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).find( + editor => editor.editor instanceof NotebookEditorInput && editor.editor.viewType === context.notebookEditor.viewModel.viewType && editor.editor.resource.toString() === context.notebookEditor.viewModel.uri.toString()); const editorGroupService = accessor.get(IEditorGroupsService); - const group = editorGroupService.activeGroup; - if (group) { - if (group.activeEditor) { - group.pinEditor(group.activeEditor); - } + if (editor) { + const group = editorGroupService.getGroup(editor.groupId); + group?.pinEditor(editor.editor); } return context.notebookEditor.executeNotebook(); @@ -488,9 +598,51 @@ registerAction2(class extends NotebookAction { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), + description: { + description: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), + args: [ + { + name: 'uri', + description: 'The document uri', + constraint: URI + } + ] + }, }); } + getEditorContextFromArgsOrActive(accessor: ServicesAccessor, context?: UriComponents): INotebookActionContext | undefined { + if (context) { + const uri = URI.revive(context); + + if (uri) { + const widget = getWidgetFromUri(accessor, uri); + + if (widget) { + return { + notebookEditor: widget, + }; + } + } + } + + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return undefined; + } + + if (!editor.hasModel()) { + return undefined; + } + + const activeCell = editor.getActiveCell(); + return { + cell: activeCell, + notebookEditor: editor + }; + } + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { return context.notebookEditor.cancelNotebookExecution(); } @@ -581,7 +733,7 @@ registerAction2(class extends NotebookCellAction { export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { // TODO@roblourens can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? - const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined; + const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean; } | undefined; return activeEditorPane?.isNotebookEditor ? (editorService.activeEditorPane?.getControl() as INotebookEditor) : undefined; } @@ -710,7 +862,7 @@ registerAction2(class extends NotebookAction { } async run(accessor: ServicesAccessor): Promise { - const context = this.getActiveEditorContext(accessor); + const context = this.getEditorContextFromArgsOrActive(accessor); if (context) { this.runWithContext(accessor, context); } @@ -735,7 +887,7 @@ registerAction2(class extends NotebookAction { } async run(accessor: ServicesAccessor): Promise { - const context = this.getActiveEditorContext(accessor); + const context = this.getEditorContextFromArgsOrActive(accessor); if (context) { this.runWithContext(accessor, context); } @@ -1892,8 +2044,8 @@ CommandsRegistry.registerCommand('notebook.trust', (accessor, args) => { CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, args): { viewType: string; displayName: string; - options: { transientOutputs: boolean; transientMetadata: TransientMetadata }; - filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern })[] + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; + filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; }[] => { const notebookService = accessor.get(INotebookService); const contentProviders = notebookService.getContributedNotebookProviders(); @@ -1915,7 +2067,7 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a } return null; - }).filter(pattern => pattern !== null) as (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern })[]; + }).filter(pattern => pattern !== null) as (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; return { viewType: provider.id, @@ -1925,3 +2077,44 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a }; }); }); + +CommandsRegistry.registerCommand('_resolveNotebookKernelProviders', async (accessor, args): Promise<{ + extensionId: string; + description?: string; + selector: INotebookDocumentFilter; +}[]> => { + const notebookService = accessor.get(INotebookService); + const providers = await notebookService.getContributedNotebookKernelProviders(); + return providers.map(provider => ({ + extensionId: provider.providerExtensionId, + description: provider.providerDescription, + selector: provider.selector + })); +}); + +CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, args: { + viewType: string; + uri: UriComponents; +}): Promise<{ + id?: string; + label: string; + description?: string; + detail?: string; + isPreferred?: boolean; + preloads?: URI[]; +}[]> => { + const notebookService = accessor.get(INotebookService); + const uri = URI.revive(args.uri as UriComponents); + const source = new CancellationTokenSource(); + const kernels = await notebookService.getContributedNotebookKernels(args.viewType, uri, source.token); + source.dispose(); + + return kernels.map(provider => ({ + id: provider.friendlyId, + label: provider.label, + description: provider.description, + detail: provider.detail, + isPreferred: provider.isPreferred, + preloads: provider.preloads?.map(preload => URI.revive(preload)) || [] + })); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index c8f3928512..5c5851ffe3 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/notebookFind'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellEditState, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; @@ -43,6 +43,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _currentMatchDecorations: ICellModelDecorations[] = []; private _showTimeout: number | null = null; private _hideTimeout: number | null = null; + private _previousFocusElement?: HTMLElement; constructor( private readonly _notebookEditor: INotebookEditor, @@ -65,6 +66,10 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote this._register(this._state.onFindReplaceStateChange(() => { this.onInputChanged(); })); + + this._register(DOM.addDisposableListener(this.getDomNode(), DOM.EventType.FOCUS, e => { + this._previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; + }, true)); } private _onFindInputKeyDown(e: IKeyboardEvent): void { @@ -167,6 +172,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } protected onFocusTrackerBlur() { + this._previousFocusElement = undefined; this._findWidgetFocused.reset(); } @@ -324,6 +330,11 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } else { // no op } + + if (this._previousFocusElement && this._previousFocusElement.offsetParent) { + this._previousFocusElement.focus(); + this._previousFocusElement = undefined; + } } clear() { @@ -374,7 +385,7 @@ registerAction2(class extends Action2 { id: 'notebook.find', title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, keybinding: { - when: NOTEBOOK_EDITOR_FOCUSED, + when: ContextKeyExpr.or(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_OPEN), primary: KeyCode.KEY_F | KeyMod.CtrlCmd, weight: KeybindingWeight.WorkbenchContrib } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css new file mode 100644 index 0000000000..e8459552e4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.css @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-list .notebook-outline-element { + display: flex; + flex: 1; + flex-flow: row nowrap; + align-items: center; +} + +.monaco-list .notebook-outline-element > .element-icon.file-icon { + height: 100%; +} + +.monaco-breadcrumbs > .notebook-outline-element > .element-icon.file-icon { + height: 18px; +} +.monaco-list .notebook-outline-element .monaco-highlighted-label { + color: var(--outline-element-color); +} + +.monaco-breadcrumbs .notebook-outline-element .element-decoration, +.monaco-list .notebook-outline-element > .element-decoration { + opacity: 0.75; + font-size: 90%; + font-weight: 600; + padding: 0 12px 0 5px; + margin-left: auto; + text-align: center; + color: var(--outline-element-color); +} + +.monaco-list .notebook-outline-element > .element-decoration.bubble { + font-family: codicon; + font-size: 14px; + opacity: 0.4; + padding-right: 8px; +} + +.monaco-breadcrumbs .notebook-outline-element .element-decoration { + /* Don't show markers inline with breadcrumbs */ + display: none; +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts new file mode 100644 index 0000000000..bab79710d8 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -0,0 +1,607 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./notebookOutline'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { combinedDisposable, IDisposable, Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { getIconClassesForModeId } from 'vs/editor/common/services/getIconClasses'; +import { IWorkbenchDataTreeOptions } from 'vs/platform/list/browser/listService'; +import { localize } from 'vs/nls'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { isEqual } from 'vs/base/common/resources'; +import { IdleValue } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import * as marked from 'vs/base/common/marked/marked'; +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; + +export interface IOutlineMarkerInfo { + readonly count: number; + readonly topSev: MarkerSeverity; +} + +export class OutlineEntry { + + private _children: OutlineEntry[] = []; + private _parent: OutlineEntry | undefined; + private _markerInfo: IOutlineMarkerInfo | undefined; + + constructor( + readonly index: number, + readonly level: number, + readonly cell: ICellViewModel, + readonly label: string, + readonly icon: ThemeIcon + ) { } + + addChild(entry: OutlineEntry) { + this._children.push(entry); + entry._parent = this; + } + + get parent(): OutlineEntry | undefined { + return this._parent; + } + + get children(): Iterable { + return this._children; + } + + get markerInfo(): IOutlineMarkerInfo | undefined { + return this._markerInfo; + } + + updateMarkers(markerService: IMarkerService): void { + if (this.cell.cellKind === CellKind.Code) { + // a code cell can have marker + const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); + if (marker.length === 0) { + this._markerInfo = undefined; + } else { + const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; + this._markerInfo = { topSev, count: marker.length }; + } + } else { + // a markdown cell can inherit markers from its children + let topChild: MarkerSeverity | undefined; + for (let child of this.children) { + child.updateMarkers(markerService); + if (child.markerInfo) { + topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); + } + } + this._markerInfo = topChild && { topSev: topChild, count: 0 }; + } + } + + clearMarkers(): void { + this._markerInfo = undefined; + for (let child of this.children) { + child.clearMarkers(); + } + } + + find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { + if (cell.id === this.cell.id) { + return this; + } + parents.push(this); + for (let child of this.children) { + const result = child.find(cell, parents); + if (result) { + return result; + } + } + parents.pop(); + return undefined; + } + + asFlatList(bucket: OutlineEntry[]): void { + bucket.push(this); + for (let child of this.children) { + child.asFlatList(bucket); + } + } +} + +class NotebookOutlineTemplate { + + static readonly templateId = 'NotebookOutlineRenderer'; + + constructor( + readonly container: HTMLElement, + readonly iconClass: HTMLElement, + readonly iconLabel: IconLabel, + readonly decoration: HTMLElement + ) { } +} + +class NotebookOutlineRenderer implements ITreeRenderer { + + templateId: string = NotebookOutlineTemplate.templateId; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { } + + renderTemplate(container: HTMLElement): NotebookOutlineTemplate { + container.classList.add('notebook-outline-element', 'show-file-icons'); + const iconClass = document.createElement('div'); + container.append(iconClass); + const iconLabel = new IconLabel(container, { supportHighlights: true }); + const decoration = document.createElement('div'); + decoration.className = 'element-decoration'; + container.append(decoration); + return new NotebookOutlineTemplate(container, iconClass, iconLabel, decoration); + } + + renderElement(node: ITreeNode, _index: number, template: NotebookOutlineTemplate, _height: number | undefined): void { + const options: IIconLabelValueOptions = { + matches: createMatches(node.filterData), + extraClasses: [] + }; + + if (node.element.cell.cellKind === CellKind.Code && this._themeService.getFileIconTheme().hasFileIcons) { + template.iconClass.className = ''; + options.extraClasses?.push(...getIconClassesForModeId(node.element.cell.language ?? '')); + } else { + template.iconClass.className = 'element-icon ' + ThemeIcon.asClassNameArray(node.element.icon).join(' '); + } + + template.iconLabel.setLabel(node.element.label, undefined, options); + + const { markerInfo } = node.element; + + template.container.style.removeProperty('--outline-element-color'); + template.decoration.innerText = ''; + if (markerInfo) { + const useBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); + if (!useBadges) { + template.decoration.classList.remove('bubble'); + template.decoration.innerText = ''; + } else if (markerInfo.count === 0) { + template.decoration.classList.add('bubble'); + template.decoration.innerText = '\uea71'; + } else { + template.decoration.classList.remove('bubble'); + template.decoration.innerText = markerInfo.count > 9 ? '9+' : String(markerInfo.count); + } + const color = this._themeService.getColorTheme().getColor(markerInfo.topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground); + const useColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); + if (!useColors) { + template.container.style.removeProperty('--outline-element-color'); + template.decoration.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit'); + } else { + template.container.style.setProperty('--outline-element-color', color?.toString() ?? 'inherit'); + } + } + } + + disposeTemplate(templateData: NotebookOutlineTemplate): void { + templateData.iconLabel.dispose(); + } +} + +class NotebookOutlineAccessibility implements IListAccessibilityProvider { + getAriaLabel(element: OutlineEntry): string | null { + return element.label; + } + getWidgetAriaLabel(): string { + return ''; + } +} + +class NotebookNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + getKeyboardNavigationLabel(element: OutlineEntry): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined { + return element.label; + } +} + +class NotebookOutlineVirtualDelegate implements IListVirtualDelegate { + + getHeight(_element: OutlineEntry): number { + return 22; + } + + getTemplateId(_element: OutlineEntry): string { + return NotebookOutlineTemplate.templateId; + } +} + +class NotebookQuickPickProvider implements IQuickPickDataSource { + + constructor( + private _getEntries: () => OutlineEntry[], + @IThemeService private readonly _themeService: IThemeService + ) { } + + getQuickPickElements(): Iterable> { + const bucket: OutlineEntry[] = []; + for (let entry of this._getEntries()) { + entry.asFlatList(bucket); + } + const result: IQuickPickOutlineElement[] = []; + const { hasFileIcons } = this._themeService.getFileIconTheme(); + for (let element of bucket) { + // todo@jrieken it is fishy that codicons cannot be used with iconClasses + // but file icons can... + result.push({ + element, + label: hasFileIcons ? element.label : `$(${element.icon.id}) ${element.label}`, + ariaLabel: element.label, + iconClasses: hasFileIcons ? getIconClassesForModeId(element.cell.language ?? '') : undefined, + }); + } + return result; + } +} + +class NotebookComparator implements IOutlineComparator { + + private readonly _collator = new IdleValue(() => new Intl.Collator(undefined, { numeric: true })); + + compareByPosition(a: OutlineEntry, b: OutlineEntry): number { + return a.index - b.index; + } + compareByType(a: OutlineEntry, b: OutlineEntry): number { + return a.cell.cellKind - b.cell.cellKind || this._collator.value.compare(a.label, b.label); + } + compareByName(a: OutlineEntry, b: OutlineEntry): number { + return this._collator.value.compare(a.label, b.label); + } +} + +class NotebookCellOutline implements IOutline { + + private readonly _dispoables = new DisposableStore(); + + private readonly _onDidChange = new Emitter(); + + readonly onDidChange: Event = this._onDidChange.event; + + private _entries: OutlineEntry[] = []; + private _activeEntry?: OutlineEntry; + private readonly _entriesDisposables = new DisposableStore(); + + readonly config: IOutlineListConfig; + readonly outlineKind = 'notebookCells'; + + get activeElement(): OutlineEntry | undefined { + return this._activeEntry; + } + + constructor( + private readonly _editor: NotebookEditor, + private readonly _target: OutlineTarget, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IEditorService private readonly _editorService: IEditorService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + const selectionListener = new MutableDisposable(); + this._dispoables.add(selectionListener); + const installSelectionListener = () => { + if (!_editor.viewModel) { + selectionListener.clear(); + } else { + selectionListener.value = combinedDisposable( + _editor.viewModel.onDidChangeSelection(() => this._recomputeActive()), + _editor.viewModel.onDidChangeViewCells(() => this._recomputeState()) + ); + } + }; + + this._dispoables.add(_editor.onDidChangeModel(() => { + this._recomputeState(); + installSelectionListener(); + })); + + this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('notebook.outline.showCodeCells')) { + this._recomputeState(); + } + })); + + this._dispoables.add(themeService.onDidFileIconThemeChange(() => { + this._onDidChange.fire({}); + })); + + this._recomputeState(); + installSelectionListener(); + + const options: IWorkbenchDataTreeOptions = { + collapseByDefault: _target === OutlineTarget.Breadcrumbs, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + accessibilityProvider: new NotebookOutlineAccessibility(), + identityProvider: { getId: element => element.cell.id }, + keyboardNavigationLabelProvider: new NotebookNavigationLabelProvider() + }; + + const treeDataSource: IDataSource = { getChildren: parent => parent instanceof NotebookCellOutline ? this._entries : parent.children }; + const delegate = new NotebookOutlineVirtualDelegate(); + const renderers = [instantiationService.createInstance(NotebookOutlineRenderer)]; + const comparator = new NotebookComparator(); + + this.config = { + breadcrumbsDataSource: { + getBreadcrumbElements: () => { + let result: OutlineEntry[] = []; + let candidate = this._activeEntry; + while (candidate) { + result.unshift(candidate); + candidate = candidate.parent; + } + return result; + } + }, + quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => this._entries), + treeDataSource, + delegate, + renderers, + comparator, + options + }; + } + + dispose(): void { + this._dispoables.dispose(); + this._entriesDisposables.dispose(); + } + + private _recomputeState(): void { + this._entriesDisposables.clear(); + this._activeEntry = undefined; + this._entries.length = 0; + + const { viewModel } = this._editor; + if (!viewModel) { + return; + } + + let includeCodeCells = true; + if (this._target === OutlineTarget.OutlinePane) { + includeCodeCells = this._configurationService.getValue('notebook.outline.showCodeCells'); + } else if (this._target === OutlineTarget.Breadcrumbs) { + includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); + } + + const [selected] = viewModel.selectionHandles; + const entries: OutlineEntry[] = []; + + for (let i = 0; i < viewModel.viewCells.length; i++) { + const cell = viewModel.viewCells[i]; + const isMarkdown = cell.cellKind === CellKind.Markdown; + if (!isMarkdown && !includeCodeCells) { + continue; + } + + // anaslse cell text but cap it 10000 characters + let content = cell.getText().substr(0, 10_000); + let level = 7; + + if (isMarkdown) { + // MD cell: "render" as plain text, find highest header + for (const token of marked.lexer(content, { gfm: true })) { + if (token.type === 'heading') { + level = Math.min(level, token.depth); + } + } + content = renderMarkdownAsPlaintext({ value: content }); + } + + // find first none empty line or use default text + const lineMatch = content.match(/^.*\w+.*\w*$/m); + let preview: string; + if (!lineMatch) { + preview = localize('empty', "empty cell"); + } else { + preview = lineMatch[0].trim(); + if (preview.length >= 64) { + preview = preview.slice(0, 64) + '…'; + } + } + + const entry = new OutlineEntry(i, level, cell, preview, isMarkdown ? Codicon.markdown : Codicon.code); + entries.push(entry); + if (cell.handle === selected) { + this._activeEntry = entry; + } + + // send an event whenever any of the cells change + this._entriesDisposables.add(cell.model.onDidChangeContent(() => { + this._recomputeState(); + this._onDidChange.fire({}); + })); + } + + // build a tree from the list of entries + if (entries.length > 0) { + let result: OutlineEntry[] = [entries[0]]; + let parentStack: OutlineEntry[] = [entries[0]]; + + for (let i = 1; i < entries.length; i++) { + let entry = entries[i]; + + while (true) { + const len = parentStack.length; + if (len === 0) { + // root node + result.push(entry); + parentStack.push(entry); + break; + + } else { + let parentCandidate = parentStack[len - 1]; + if (parentCandidate.level < entry.level) { + parentCandidate.addChild(entry); + parentStack.push(entry); + break; + } else { + parentStack.pop(); + } + } + } + } + this._entries = result; + } + + // feature: show markers with each cell + const markerServiceListener = new MutableDisposable(); + this._entriesDisposables.add(markerServiceListener); + const updateMarkerUpdater = () => { + const doUpdateMarker = (clear: boolean) => { + for (let entry of this._entries) { + if (clear) { + entry.clearMarkers(); + } else { + entry.updateMarkers(this._markerService); + } + } + }; + if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { + markerServiceListener.value = this._markerService.onMarkerChanged(e => { + if (e.some(uri => viewModel.viewCells.some(cell => isEqual(cell.uri, uri)))) { + doUpdateMarker(false); + this._onDidChange.fire({}); + } + }); + doUpdateMarker(false); + } else { + markerServiceListener.clear(); + doUpdateMarker(true); + } + }; + updateMarkerUpdater(); + this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { + updateMarkerUpdater(); + this._onDidChange.fire({}); + } + })); + + this._onDidChange.fire({}); + } + + private _recomputeActive(): void { + let newActive: OutlineEntry | undefined; + const { viewModel } = this._editor; + + if (viewModel) { + const [selected] = viewModel.selectionHandles; + const cell = viewModel.getCellByHandle(selected); + if (cell) { + for (let entry of this._entries) { + newActive = entry.find(cell, []); + if (newActive) { + break; + } + } + } + } + if (newActive !== this._activeEntry) { + this._activeEntry = newActive; + this._onDidChange.fire({ affectOnlyActiveElement: true }); + } + } + + get isEmpty(): boolean { + return this._entries.length === 0; + } + + async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise { + await this._editorService.openEditor({ + resource: entry.cell.uri, + options, + }, sideBySide ? SIDE_GROUP : undefined); + } + + preview(entry: OutlineEntry): IDisposable { + const widget = this._editor.getControl(); + if (!widget) { + return Disposable.None; + } + widget.revealInCenterIfOutsideViewport(entry.cell); + const ids = widget.deltaCellDecorations([], [{ + handle: entry.cell.handle, + options: { className: 'nb-symbolHighlight', outputClassName: 'nb-symbolHighlight' } + }]); + return toDisposable(() => { widget.deltaCellDecorations(ids, []); }); + + } + + captureViewState(): IDisposable { + const widget = this._editor.getControl(); + let viewState = widget?.getEditorViewState(); + return toDisposable(() => { + if (viewState) { + widget?.restoreListViewState(viewState); + } + }); + } +} + +class NotebookOutlineCreator implements IOutlineCreator { + + readonly dispose: () => void; + + constructor( + @IOutlineService outlineService: IOutlineService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + const reg = outlineService.registerOutlineCreator(this); + this.dispose = () => reg.dispose(); + } + + matches(candidate: IEditorPane): candidate is NotebookEditor { + return candidate.getId() === NotebookEditor.ID; + } + + async createOutline(editor: NotebookEditor, target: OutlineTarget): Promise | undefined> { + return this._instantiationService.createInstance(NotebookCellOutline, editor, target); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookOutlineCreator, LifecyclePhase.Eventually); + + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'notebook', + order: 100, + type: 'object', + 'properties': { + 'notebook.outline.showCodeCells': { + type: 'boolean', + default: false, + markdownDescription: localize('outline.showCodeCells', "When enabled notebook outline shows code cells.") + }, + 'notebook.breadcrumbs.showCodeCells': { + type: 'boolean', + default: true, + markdownDescription: localize('breadcrumbs.showCodeCells', "When enabled notebook breadcrumbs contain code cells.") + }, + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts deleted file mode 100644 index c85b24e4c8..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser'; -import { registerNotebookContribution } from '../../notebookEditorExtensions'; -import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; -import { createProviderComparer } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; -import { first, ThrottledDelayer } from 'vs/base/common/async'; -import { INotebookService } from '../../../common/notebookService'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; - -export class SCMController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.findController'; - private _lastDecorationId: string[] = []; - private _localDisposable = new DisposableStore(); - private _originalDocument: NotebookTextModel | undefined = undefined; - private _originalResourceDisposableStore = new DisposableStore(); - private _diffDelayer = new ThrottledDelayer(200); - - private _lastVersion = -1; - - - constructor( - private readonly _notebookEditor: INotebookEditor, - @IFileService private readonly _fileService: FileService, - @ISCMService private readonly _scmService: ISCMService, - @INotebookService private readonly _notebookService: INotebookService - - ) { - super(); - - if (!this._notebookEditor.isEmbedded) { - this._register(this._notebookEditor.onDidChangeModel(() => { - this._localDisposable.clear(); - this._originalResourceDisposableStore.clear(); - this._diffDelayer.cancel(); - this.update(); - - if (this._notebookEditor.textModel) { - this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent((e) => { - this.update(); - })); - } - })); - - this._register(this._notebookEditor.onWillDispose(() => { - this._localDisposable.clear(); - this._originalResourceDisposableStore.clear(); - })); - - this.update(); - } - } - - private async _resolveNotebookDocument(uri: URI, viewType: string) { - const providers = this._scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); - - rootedProviders.sort(createProviderComparer(uri)); - - const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); - - if (!result) { - this._originalDocument = undefined; - this._originalResourceDisposableStore.clear(); - return; - } - - if (result.toString() === this._originalDocument?.uri.toString()) { - // original document not changed - return; - } - - this._originalResourceDisposableStore.add(this._fileService.watch(result)); - this._originalResourceDisposableStore.add(this._fileService.onDidFilesChange(e => { - if (e.contains(result)) { - this._originalDocument = undefined; - this._originalResourceDisposableStore.clear(); - this.update(); - } - })); - - const originalDocument = await this._notebookService.resolveNotebook(viewType, result, false); - this._originalResourceDisposableStore.add({ - dispose: () => { - this._originalDocument?.dispose(); - this._originalDocument = undefined; - } - }); - - this._originalDocument = originalDocument; - } - - async update() { - if (!this._diffDelayer) { - return; - } - - await this._diffDelayer - .trigger(async () => { - const modifiedDocument = this._notebookEditor.textModel; - if (!modifiedDocument) { - return; - } - - if (this._lastVersion >= modifiedDocument.versionId) { - return; - } - - this._lastVersion = modifiedDocument.versionId; - await this._resolveNotebookDocument(modifiedDocument.uri, modifiedDocument.viewType); - - if (!this._originalDocument) { - this._clear(); - return; - } - - // const diff = new LcsDiff(new CellSequence(this._originalDocument), new CellSequence(modifiedDocument)); - // const diffResult = diff.ComputeDiff(false); - - // const decorations: INotebookDeltaDecoration[] = []; - // diffResult.changes.forEach(change => { - // if (change.originalLength === 0) { - // // doesn't exist in original - // for (let i = 0; i < change.modifiedLength; i++) { - // decorations.push({ - // handle: modifiedDocument.cells[change.modifiedStart + i].handle, - // options: { gutterClassName: 'nb-gutter-cell-inserted' } - // }); - // } - // } else { - // if (change.modifiedLength === 0) { - // // diff.deleteCount - // // removed from original - // } else { - // // modification - // for (let i = 0; i < change.modifiedLength; i++) { - // decorations.push({ - // handle: modifiedDocument.cells[change.modifiedStart + i].handle, - // options: { gutterClassName: 'nb-gutter-cell-changed' } - // }); - // } - // } - // } - // }); - - - // this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, decorations); - }); - } - - private _clear() { - this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, []); - } -} - -registerNotebookContribution(SCMController.id, SCMController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index bcc2f70869..e8f727d7a7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -8,7 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { INotebookActionContext, NOTEBOOK_ACTIONS_CATEGORY, getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { NOTEBOOK_ACTIONS_CATEGORY, getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -33,11 +33,33 @@ registerAction2(class extends Action2 { title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: selectKernelIcon, - f1: true + f1: true, + description: { + description: nls.localize('notebookActions.selectKernel.args', "Notebook Kernel Args"), + args: [ + { + name: 'kernelInfo', + description: 'The kernel info', + schema: { + 'type': 'object', + 'required': ['id', 'extension'], + 'properties': { + 'id': { + 'type': 'string' + }, + 'extension': { + 'type': 'string' + } + } + } + } + ] + }, + }); } - async run(accessor: ServicesAccessor, context?: INotebookActionContext): Promise { + async run(accessor: ServicesAccessor, context?: { id: string, extension: string }): Promise { const editorService = accessor.get(IEditorService); const quickInputService = accessor.get(IQuickInputService); const configurationService = accessor.get(IConfigurationService); @@ -52,20 +74,38 @@ registerAction2(class extends Action2 { const picker = quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); picker.matchOnDetail = true; - picker.show(); + + + if (context && context.id) { + } else { + picker.show(); + } + picker.busy = true; const tokenSource = new CancellationTokenSource(); - const availableKernels2 = await editor.beginComputeContributedKernels(); - const picks: QuickPickInput[] = [...availableKernels2].map((a) => { + const availableKernels = await editor.beginComputeContributedKernels(); + + const selectedKernel = availableKernels.length ? availableKernels.find( + kernel => kernel.id && context?.id && kernel.id === context?.id && kernel.extension.value === context?.extension + ) : undefined; + + if (selectedKernel) { + editor.activeKernel = selectedKernel!; + return selectedKernel!.resolve(editor.uri!, editor.getId(), tokenSource.token); + } else { + picker.show(); + } + + const picks: QuickPickInput[] = [...availableKernels].map((a) => { return { - id: a.id, + id: a.friendlyId, label: a.label, - picked: a.id === activeKernel?.id, + picked: a.friendlyId === activeKernel?.friendlyId, description: a.description ? a.description - : a.extension.value + (a.id === activeKernel?.id + : a.extension.value + (a.friendlyId === activeKernel?.friendlyId ? nls.localize('currentActiveKernel', " (Currently Active)") : ''), detail: a.detail, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts deleted file mode 100644 index 2871d60b6b..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { TableOfContentsProviderRegistry, ITableOfContentsProvider, ITableOfContentsEntry } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; -import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Codicon } from 'vs/base/common/codicons'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; - -TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements ITableOfContentsProvider { - async provideTableOfContents(editor: NotebookEditor, context: { disposables: DisposableStore }) { - if (!editor.viewModel) { - return undefined; - } - // return an entry per markdown header - const notebookWidget = editor.getControl(); - if (!notebookWidget) { - return undefined; - } - - // restore initial view state when no item was picked - let didPickOne = false; - const viewState = notebookWidget.getEditorViewState(); - context.disposables.add(toDisposable(() => { - if (!didPickOne) { - notebookWidget.restoreListViewState(viewState); - } - })); - - let lastDecorationId: string[] = []; - const result: ITableOfContentsEntry[] = []; - for (const cell of editor.viewModel.viewCells) { - const content = cell.getText(); - const regexp = cell.cellKind === CellKind.Markdown - ? /^[ \t]*(\#+)(.+)$/gm // md: header - : /^.*\w+.*\w*$/m; // code: none empty line - - const matches = content.match(regexp); - if (matches && matches.length) { - for (let j = 0; j < matches.length; j++) { - result.push({ - icon: cell.cellKind === CellKind.Markdown ? Codicon.markdown : Codicon.code, - label: matches[j].replace(/^[ \t]*(\#+)/, ''), - pick() { - didPickOne = true; - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - notebookWidget.focusNotebookCell(cell, cell.cellKind === CellKind.Markdown ? 'container' : 'editor'); - lastDecorationId = notebookWidget.deltaCellDecorations(lastDecorationId, []); - }, - preview() { - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - lastDecorationId = notebookWidget.deltaCellDecorations(lastDecorationId, [{ - handle: cell.handle, - options: { className: 'nb-symbolHighlight', outputClassName: 'nb-symbolHighlight' } - }]); - } - }); - } - } - } - - context.disposables.add(toDisposable(() => { - notebookWidget.deltaCellDecorations(lastDecorationId, []); - })); - - return result; - } -}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts deleted file mode 100644 index dc4f2db603..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ /dev/null @@ -1,1091 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; -import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { format } from 'vs/base/common/jsonFormatter'; -import { applyEdits } from 'vs/base/common/jsonEdit'; -import { CellEditType, CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { hash } from 'vs/base/common/hash'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IAction } from 'vs/base/common/actions'; -import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; -import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { renderCodicons } from 'vs/base/browser/codicons'; - -const fixedEditorOptions: IEditorOptions = { - padding: { - top: 12, - bottom: 12 - }, - scrollBeyondLastLine: false, - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false - }, - renderLineHighlightOnlyWhenFocus: true, - overviewRulerLanes: 0, - selectOnLineNumbers: false, - wordWrap: 'off', - lineNumbers: 'off', - lineDecorationsWidth: 0, - glyphMargin: false, - fixedOverflowWidgets: true, - minimap: { enabled: false }, - renderValidationDecorations: 'on', - renderLineHighlight: 'none', - readOnly: true -}; - -const fixedDiffEditorOptions: IDiffEditorOptions = { - ...fixedEditorOptions, - glyphMargin: true, - enableSplitViewResizing: false, - renderIndicators: false, - readOnly: false, - isInEmbeddedEditor: true -}; - - - -class PropertyHeader extends Disposable { - protected _foldingIndicator!: HTMLElement; - protected _statusSpan!: HTMLElement; - protected _toolbar!: ToolBar; - protected _menu!: IMenu; - - constructor( - readonly cell: CellDiffViewModel, - readonly metadataHeaderContainer: HTMLElement, - readonly notebookEditor: INotebookTextDiffEditor, - readonly accessor: { - updateInfoRendering: () => void; - checkIfModified: (cell: CellDiffViewModel) => boolean; - getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState; - updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void; - unChangedLabel: string; - changedLabel: string; - prefix: string; - menuId: MenuId; - }, - @IContextMenuService readonly contextMenuService: IContextMenuService, - @IKeybindingService readonly keybindingService: IKeybindingService, - @INotificationService readonly notificationService: INotificationService, - @IMenuService readonly menuService: IMenuService, - @IContextKeyService readonly contextKeyService: IContextKeyService - ) { - super(); - } - - buildHeader(): void { - let metadataChanged = this.accessor.checkIfModified(this.cell); - this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator')); - this._foldingIndicator.classList.add(this.accessor.prefix); - this._updateFoldingIcon(); - const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status')); - this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); - - if (metadataChanged) { - this._statusSpan.textContent = this.accessor.changedLabel; - this._statusSpan.style.fontWeight = 'bold'; - this.metadataHeaderContainer.classList.add('modified'); - } else { - this._statusSpan.textContent = this.accessor.unChangedLabel; - } - - const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar')); - this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); - return item; - } - - return undefined; - } - }); - this._register(this._toolbar); - this._toolbar.context = { - cell: this.cell - }; - - this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService); - this._register(this._menu); - - if (metadataChanged) { - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); - this._toolbar.setActions(actions); - } - - this._register(this.notebookEditor.onMouseUp(e => { - if (!e.event.target) { - return; - } - - const target = e.event.target as HTMLElement; - - if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { - const parent = target.parentElement as HTMLElement; - - if (!parent) { - return; - } - - if (!parent.classList.contains(this.accessor.prefix)) { - return; - } - - if (!parent.classList.contains('property-folding-indicator')) { - return; - } - - // folding icon - - const cellViewModel = e.target; - - if (cellViewModel === this.cell) { - const oldFoldingState = this.accessor.getFoldingState(this.cell); - this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); - this._updateFoldingIcon(); - this.accessor.updateInfoRendering(); - } - } - - return; - })); - - this._updateFoldingIcon(); - this.accessor.updateInfoRendering(); - } - - refresh() { - let metadataChanged = this.accessor.checkIfModified(this.cell); - if (metadataChanged) { - this._statusSpan.textContent = this.accessor.changedLabel; - this._statusSpan.style.fontWeight = 'bold'; - this.metadataHeaderContainer.classList.add('modified'); - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, undefined, actions); - this._toolbar.setActions(actions); - } else { - this._statusSpan.textContent = this.accessor.unChangedLabel; - this._statusSpan.style.fontWeight = 'normal'; - this._toolbar.setActions([]); - } - } - - private _updateFoldingIcon() { - if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { - DOM.reset(this._foldingIndicator, ...renderCodicons(ThemeIcon.asCodiconLabel(collapsedIcon))); - } else { - DOM.reset(this._foldingIndicator, ...renderCodicons(ThemeIcon.asCodiconLabel(expandedIcon))); - } - } -} - -abstract class AbstractCellRenderer extends Disposable { - protected _metadataHeaderContainer!: HTMLElement; - protected _metadataHeader!: PropertyHeader; - protected _metadataInfoContainer!: HTMLElement; - protected _metadataEditorContainer?: HTMLElement; - protected _metadataEditorDisposeStore!: DisposableStore; - protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; - - protected _outputHeaderContainer!: HTMLElement; - protected _outputHeader!: PropertyHeader; - protected _outputInfoContainer!: HTMLElement; - protected _outputEditorContainer?: HTMLElement; - protected _outputEditorDisposeStore!: DisposableStore; - protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; - - - protected _diffEditorContainer!: HTMLElement; - protected _diagonalFill?: HTMLElement; - protected _layoutInfo!: { - editorHeight: number; - editorMargin: number; - metadataStatusHeight: number; - metadataHeight: number; - outputStatusHeight: number; - outputHeight: number; - bodyMargin: number; - }; - protected _isDisposed: boolean; - - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - readonly style: 'left' | 'right' | 'full', - protected readonly instantiationService: IInstantiationService, - protected readonly modeService: IModeService, - protected readonly modelService: IModelService, - protected readonly contextMenuService: IContextMenuService, - protected readonly keybindingService: IKeybindingService, - protected readonly notificationService: INotificationService, - protected readonly menuService: IMenuService, - protected readonly contextKeyService: IContextKeyService - - - ) { - super(); - // init - this._isDisposed = false; - this._layoutInfo = { - editorHeight: 0, - editorMargin: 0, - metadataHeight: 0, - metadataStatusHeight: 25, - outputHeight: 0, - outputStatusHeight: 25, - bodyMargin: 32 - }; - this._metadataEditorDisposeStore = new DisposableStore(); - this._outputEditorDisposeStore = new DisposableStore(); - this._register(this._metadataEditorDisposeStore); - this.initData(); - this.buildBody(templateData.container); - this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e))); - } - - buildBody(container: HTMLElement) { - const body = DOM.$('.cell-body'); - DOM.append(container, body); - this._diffEditorContainer = DOM.$('.cell-diff-editor-container'); - switch (this.style) { - case 'left': - body.classList.add('left'); - break; - case 'right': - body.classList.add('right'); - break; - default: - body.classList.add('full'); - break; - } - - DOM.append(body, this._diffEditorContainer); - this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill')); - this.styleContainer(this._diffEditorContainer); - const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container')); - this.buildSourceEditor(sourceContainer); - - this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); - this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); - - const checkIfModified = (cell: CellDiffViewModel) => { - return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); - }; - - if (checkIfModified(this.cell)) { - this.cell.metadataFoldingState = PropertyFoldingState.Expanded; - } - - this._metadataHeader = this.instantiationService.createInstance( - PropertyHeader, - this.cell, - this._metadataHeaderContainer, - this.notebookEditor, - { - updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return checkIfModified(cell); - }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; - }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; - }, - unChangedLabel: 'Metadata', - changedLabel: 'Metadata changed', - prefix: 'metadata', - menuId: MenuId.NotebookDiffCellMetadataTitle - } - ); - this._register(this._metadataHeader); - this._metadataHeader.buildHeader(); - - if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { - this._layoutInfo.outputHeight = 0; - this._layoutInfo.outputStatusHeight = 0; - this.layout({}); - return; - } - - this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); - this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); - - const checkIfOutputsModified = (cell: CellDiffViewModel) => { - return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); - }; - - if (checkIfOutputsModified(this.cell)) { - this.cell.outputFoldingState = PropertyFoldingState.Expanded; - } - - this._outputHeader = this.instantiationService.createInstance( - PropertyHeader, - this.cell, - this._outputHeaderContainer, - this.notebookEditor, - { - updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return checkIfOutputsModified(cell); - }, - getFoldingState: (cell) => { - return cell.outputFoldingState; - }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; - }, - unChangedLabel: 'Outputs', - changedLabel: 'Outputs changed', - prefix: 'output', - menuId: MenuId.NotebookDiffCellOutputsTitle - } - ); - this._register(this._outputHeader); - this._outputHeader.buildHeader(); - } - - updateMetadataRendering() { - if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - // we should expand the metadata editor - this._metadataInfoContainer.style.display = 'block'; - - if (!this._metadataEditorContainer || !this._metadataEditor) { - // create editor - this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); - this._buildMetadataEditor(); - } else { - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - } - } else { - // we should collapse the metadata editor - this._metadataInfoContainer.style.display = 'none'; - this._metadataEditorDisposeStore.clear(); - this._layoutInfo.metadataHeight = 0; - this.layout({}); - } - } - - updateOutputRendering() { - if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._outputInfoContainer.style.display = 'block'; - - if (!this._outputEditorContainer || !this._outputEditor) { - // create editor - this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); - this._buildOutputEditor(); - } else { - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - } - } else { - this._outputInfoContainer.style.display = 'none'; - this._outputEditorDisposeStore.clear(); - this._layoutInfo.outputHeight = 0; - this.layout({}); - } - } - - protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { - let filteredMetadata: { [key: string]: any } = {}; - - if (this.notebookEditor.textModel) { - const transientMetadata = this.notebookEditor.textModel!.transientOptions.transientMetadata; - - const keys = new Set([...Object.keys(metadata)]); - for (let key of keys) { - if (!(transientMetadata[key as keyof NotebookCellMetadata]) - ) { - filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; - } - } - } else { - filteredMetadata = metadata; - } - - const content = JSON.stringify({ - language, - ...filteredMetadata - }); - - const edits = format(content, undefined, {}); - const metadataSource = applyEdits(content, edits); - - return metadataSource; - } - - private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { - let result: { [key: string]: any } = {}; - let newLangauge: string | undefined = undefined; - try { - const newMetadataObj = JSON.parse(newMetadata); - const keys = new Set([...Object.keys(newMetadataObj)]); - for (let key of keys) { - switch (key as keyof NotebookCellMetadata) { - case 'breakpointMargin': - case 'editable': - case 'hasExecutionOrder': - case 'inputCollapsed': - case 'outputCollapsed': - case 'runnable': - // boolean - if (typeof newMetadataObj[key] === 'boolean') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - - case 'executionOrder': - case 'lastRunDuration': - // number - if (typeof newMetadataObj[key] === 'number') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'runState': - // enum - if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - case 'statusMessage': - // string - if (typeof newMetadataObj[key] === 'string') { - result[key] = newMetadataObj[key]; - } else { - result[key] = currentMetadata[key as keyof NotebookCellMetadata]; - } - break; - default: - if (key === 'language') { - newLangauge = newMetadataObj[key]; - } - result[key] = newMetadataObj[key]; - break; - } - } - - if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); - this.notebookEditor.textModel!.applyEdits( - this.notebookEditor.textModel!.versionId, - [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], - true, - undefined, - () => undefined, - undefined - ); - } - - const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); - - if (index < 0) { - return; - } - - this.notebookEditor.textModel!.applyEdits(this.notebookEditor.textModel!.versionId, [ - { editType: CellEditType.Metadata, index, metadata: result } - ], true, undefined, () => undefined, undefined); - } catch { - } - } - - private _buildMetadataEditor() { - if (this.cell.type === 'modified' || this.cell.type === 'unchanged') { - const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language); - const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); - this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false, - originalEditable: false, - ignoreTrimWhitespace: false - }); - this._register(this._metadataEditor); - - this._metadataEditorContainer?.classList.add('diff'); - - const mode = this.modeService.create('json'); - const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false); - const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false); - this._metadataEditor.setModel({ - original: originalMetadataModel, - modified: modifiedMetadataModel - }); - - this._register(originalMetadataModel); - this._register(modifiedMetadataModel); - - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - - this._register(this._metadataEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.metadataHeight = e.contentHeight; - this.layout({ metadataEditor: true }); - } - })); - - let respondingToContentChange = false; - - this._register(modifiedMetadataModel.onDidChangeContent(() => { - respondingToContentChange = true; - const value = modifiedMetadataModel.getValue(); - this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); - this._metadataHeader.refresh(); - respondingToContentChange = false; - })); - - this._register(this.cell.modified!.onDidChangeMetadata(() => { - if (respondingToContentChange) { - return; - } - - const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); - modifiedMetadataModel.setValue(modifiedMetadataSource); - })); - - return; - } - - this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { - ...fixedEditorOptions, - dimension: { - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: 0 - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false - }, {}); - this._register(this._metadataEditor); - - const mode = this.modeService.create('jsonc'); - const originalMetadataSource = this._getFormatedMetadataJSON( - this.cell.type === 'insert' - ? this.cell.modified!.metadata || {} - : this.cell.original!.metadata || {}); - const uri = this.cell.type === 'insert' - ? this.cell.modified!.uri - : this.cell.original!.uri; - const handle = this.cell.type === 'insert' - ? this.cell.modified!.handle - : this.cell.original!.handle; - - const modelUri = CellUri.generateCellMetadataUri(uri, handle); - const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); - this._metadataEditor.setModel(metadataModel); - this._register(metadataModel); - - this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); - this.layout({ metadataEditor: true }); - - this._register(this._metadataEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.metadataHeight = e.contentHeight; - this.layout({ metadataEditor: true }); - } - })); - } - - private _getFormatedOutputJSON(outputs: any[]) { - const content = JSON.stringify(outputs); - - const edits = format(content, undefined, {}); - const source = applyEdits(content, edits); - - return source; - } - - private _buildOutputEditor() { - if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { - const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); - const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); - if (originalOutputsSource !== modifiedOutputsSource) { - this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: true, - ignoreTrimWhitespace: false - }); - this._register(this._outputEditor); - - this._outputEditorContainer?.classList.add('diff'); - - const mode = this.modeService.create('json'); - const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); - const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); - this._outputEditor.setModel({ - original: originalModel, - modified: modifiedModel - }); - - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - - this._register(this._outputEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.outputHeight = e.contentHeight; - this.layout({ outputEditor: true }); - } - })); - - this._register(this.cell.modified!.onDidChangeOutputs(() => { - const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); - modifiedModel.setValue(modifiedOutputsSource); - this._outputHeader.refresh(); - })); - - return; - } - } - - this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { - ...fixedEditorOptions, - dimension: { - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: 0 - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() - }, {}); - this._register(this._outputEditor); - - const mode = this.modeService.create('json'); - const originaloutputSource = this._getFormatedOutputJSON( - this.notebookEditor.textModel!.transientOptions.transientOutputs - ? [] - : this.cell.type === 'insert' - ? this.cell.modified!.outputs || [] - : this.cell.original!.outputs || []); - const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); - this._outputEditor.setModel(outputModel); - - this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); - this.layout({ outputEditor: true }); - - this._register(this._outputEditor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { - this._layoutInfo.outputHeight = e.contentHeight; - this.layout({ outputEditor: true }); - } - })); - } - - protected layoutNotebookCell() { - this.notebookEditor.layoutNotebookCell( - this.cell, - this._layoutInfo.editorHeight - + this._layoutInfo.editorMargin - + this._layoutInfo.metadataHeight - + this._layoutInfo.metadataStatusHeight - + this._layoutInfo.outputHeight - + this._layoutInfo.outputStatusHeight - + this._layoutInfo.bodyMargin - ); - } - - dispose() { - this._isDisposed = true; - super.dispose(); - } - - abstract initData(): void; - abstract styleContainer(container: HTMLElement): void; - abstract buildSourceEditor(sourceContainer: HTMLElement): void; - abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void; - abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void; -} - -export class DeletedCell extends AbstractCellRenderer { - private _editor!: CodeEditorWidget; - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - - - ) { - super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement) { - container.classList.add('removed'); - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const originalCell = this.cell.original!; - const lineCount = originalCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - - const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { - ...fixedEditorOptions, - dimension: { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() - }, {}); - this._register(this._editor); - - this._layoutInfo.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - originalCell.resolveTextModelRef().then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._layoutInfo.editorHeight = this._editor.getContentHeight(); - this.layout({ editorHeight: true }); - }); - - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editor.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.editorHeight - }); - } - - if (state.metadataEditor || state.outerWidth) { - this._metadataEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.metadataHeight - }); - } - - if (state.outputEditor || state.outerWidth) { - this._outputEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.outputHeight - }); - } - - this.layoutNotebookCell(); - }); - } -} - -export class InsertCell extends AbstractCellRenderer { - private _editor!: CodeEditorWidget; - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - ) { - super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement): void { - container.classList.add('inserted'); - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { - ...fixedEditorOptions, - dimension: { - width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, - height: editorHeight - }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - readOnly: false - }, {}); - this._register(this._editor); - - this._layoutInfo.editorHeight = editorHeight; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - modifiedCell.resolveTextModelRef().then(ref => { - if (this._isDisposed) { - return; - } - - this._register(ref); - - const textModel = ref.object.textEditorModel; - this._editor.setModel(textModel); - this._layoutInfo.editorHeight = this._editor.getContentHeight(); - this.layout({ editorHeight: true }); - }); - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editor.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), - height: this._layoutInfo.editorHeight - }); - } - - if (state.metadataEditor || state.outerWidth) { - this._metadataEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: this._layoutInfo.metadataHeight - }); - } - - if (state.outputEditor || state.outerWidth) { - this._outputEditor?.layout({ - width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), - height: this._layoutInfo.outputHeight - }); - } - - this.layoutNotebookCell(); - }); - } -} - -export class ModifiedCell extends AbstractCellRenderer { - private _editor?: DiffEditorWidget; - private _editorContainer!: HTMLElement; - private _inputToolbarContainer!: HTMLElement; - protected _toolbar!: ToolBar; - protected _menu!: IMenu; - - constructor( - readonly notebookEditor: INotebookTextDiffEditor, - readonly cell: CellDiffViewModel, - readonly templateData: CellDiffRenderTemplate, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService readonly modeService: IModeService, - @IModelService readonly modelService: IModelService, - @IContextMenuService protected readonly contextMenuService: IContextMenuService, - @IKeybindingService protected readonly keybindingService: IKeybindingService, - @INotificationService protected readonly notificationService: INotificationService, - @IMenuService protected readonly menuService: IMenuService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService - ) { - super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); - } - - initData(): void { - } - - styleContainer(container: HTMLElement): void { - } - - buildSourceEditor(sourceContainer: HTMLElement): void { - const modifiedCell = this.cell.modified!; - const lineCount = modifiedCell.textBuffer.getLineCount(); - const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; - this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); - - this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, { - ...fixedDiffEditorOptions, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), - originalEditable: false, - ignoreTrimWhitespace: false - }); - this._register(this._editor); - this._editorContainer.classList.add('diff'); - - this._editor.layout({ - width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, - height: editorHeight - }); - - this._editorContainer.style.height = `${editorHeight}px`; - - this._register(this._editor.onDidContentSizeChange((e) => { - if (e.contentHeightChanged && this._layoutInfo.editorHeight !== e.contentHeight) { - this._layoutInfo.editorHeight = e.contentHeight; - this.layout({ editorHeight: true }); - } - })); - - this._initializeSourceDiffEditor(); - - this._inputToolbarContainer = DOM.append(sourceContainer, DOM.$('.editor-input-toolbar-container')); - const cellToolbarContainer = DOM.append(this._inputToolbarContainer, DOM.$('div.property-toolbar')); - this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); - return item; - } - - return undefined; - } - }); - - this._toolbar.context = { - cell: this.cell - }; - - this._menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, this.contextKeyService); - this._register(this._menu); - const actions: IAction[] = []; - createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); - this._toolbar.setActions(actions); - - if (this.cell.modified!.getValue() !== this.cell.original!.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - } else { - this._inputToolbarContainer.style.display = 'none'; - } - - this._register(this.cell.modified!.onDidChangeContent(() => { - if (this.cell.modified!.getValue() !== this.cell.original!.getValue()) { - this._inputToolbarContainer.style.display = 'block'; - } else { - this._inputToolbarContainer.style.display = 'none'; - } - })); - } - - private async _initializeSourceDiffEditor() { - const originalCell = this.cell.original!; - const modifiedCell = this.cell.modified!; - - const originalRef = await originalCell.resolveTextModelRef(); - const modifiedRef = await modifiedCell.resolveTextModelRef(); - - if (this._isDisposed) { - return; - } - - const textModel = originalRef.object.textEditorModel; - const modifiedTextModel = modifiedRef.object.textEditorModel; - this._register(originalRef); - this._register(modifiedRef); - - this._editor!.setModel({ - original: textModel, - modified: modifiedTextModel - }); - - const contentHeight = this._editor!.getContentHeight(); - this._layoutInfo.editorHeight = contentHeight; - this.layout({ editorHeight: true }); - - } - - onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { - if (e.outerWidth !== undefined) { - this.layout({ outerWidth: true }); - } - } - - layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { - DOM.scheduleAtNextAnimationFrame(() => { - if (state.editorHeight || state.outerWidth) { - this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`; - this._editor!.layout(); - } - - if (state.metadataEditor || state.outerWidth) { - if (this._metadataEditorContainer) { - this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`; - this._metadataEditor?.layout(); - } - } - - if (state.outputEditor || state.outerWidth) { - if (this._outputEditorContainer) { - this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`; - this._outputEditor?.layout(); - } - } - - this.layoutNotebookCell(); - }); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts deleted file mode 100644 index e880450f78..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; - -export enum PropertyFoldingState { - Expanded, - Collapsed -} - -export class CellDiffViewModel extends Disposable { - public metadataFoldingState: PropertyFoldingState; - public outputFoldingState: PropertyFoldingState; - private _layoutInfoEmitter = new Emitter(); - - onDidLayoutChange = this._layoutInfoEmitter.event; - - constructor( - readonly original: NotebookCellTextModel | undefined, - readonly modified: NotebookCellTextModel | undefined, - readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', - readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher - ) { - super(); - this.metadataFoldingState = PropertyFoldingState.Collapsed; - this.outputFoldingState = PropertyFoldingState.Collapsed; - - this._register(this.editorEventDispatcher.onDidChangeLayout(e => { - this._layoutInfoEmitter.fire({ outerWidth: e.value.width }); - })); - } - - getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { - if (fullWidth) { - return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; - } - - return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/common.ts b/src/vs/workbench/contrib/notebook/browser/diff/common.ts deleted file mode 100644 index b6dc4dab7a..0000000000 --- a/src/vs/workbench/contrib/notebook/browser/diff/common.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { Event } from 'vs/base/common/event'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; - -export interface INotebookTextDiffEditor { - readonly textModel?: NotebookTextModel; - onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>; - getOverflowContainerDomNode(): HTMLElement; - getLayoutInfo(): NotebookLayoutInfo; - layoutNotebookCell(cell: CellDiffViewModel, height: number): void; -} - -export interface CellDiffRenderTemplate { - readonly container: HTMLElement; - readonly elementDisposables: DisposableStore; -} - -export interface CellDiffViewModelLayoutChangeEvent { - font?: BareFontInfo; - outerWidth?: number; -} - -export const DIFF_CELL_MARGIN = 16; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts new file mode 100644 index 0000000000..a96f5c57be --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -0,0 +1,1463 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DiffElementViewModelBase, getFormatedMetadataJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { CellEditType, CellUri, IProcessedOutput, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Delayer } from 'vs/base/common/async'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { getEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { OutputContainer } from 'vs/workbench/contrib/notebook/browser/diff/diffElementOutputs'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { AccessibilityHelpController } from 'vs/workbench/contrib/codeEditor/browser/accessibility/accessibility'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +export const fixedEditorOptions: IEditorOptions = { + padding: { + top: 12, + bottom: 12 + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + vertical: 'hidden', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false, + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + overviewRulerBorder: false, + selectOnLineNumbers: false, + wordWrap: 'off', + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + renderLineHighlight: 'none', + readOnly: true +}; + +export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + SuggestController.ID, + SnippetController2.ID, + TabCompletionController.ID, + AccessibilityHelpController.ID + ]) + }; +} + +export const fixedDiffEditorOptions: IDiffEditorOptions = { + ...fixedEditorOptions, + glyphMargin: true, + enableSplitViewResizing: false, + renderIndicators: false, + readOnly: false, + isInEmbeddedEditor: true, + renderOverviewRuler: false +}; + +class PropertyHeader extends Disposable { + protected _foldingIndicator!: HTMLElement; + protected _statusSpan!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + protected _propertyExpanded?: IContextKey; + + constructor( + readonly cell: DiffElementViewModelBase, + readonly propertyHeaderContainer: HTMLElement, + readonly notebookEditor: INotebookTextDiffEditor, + readonly accessor: { + updateInfoRendering: (renderOutput: boolean) => void; + checkIfModified: (cell: DiffElementViewModelBase) => boolean; + getFoldingState: (cell: DiffElementViewModelBase) => PropertyFoldingState; + updateFoldingState: (cell: DiffElementViewModelBase, newState: PropertyFoldingState) => void; + unChangedLabel: string; + changedLabel: string; + prefix: string; + menuId: MenuId; + }, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IKeybindingService readonly keybindingService: IKeybindingService, + @INotificationService readonly notificationService: INotificationService, + @IMenuService readonly menuService: IMenuService, + @IContextKeyService readonly contextKeyService: IContextKeyService + ) { + super(); + } + + buildHeader(): void { + let metadataChanged = this.accessor.checkIfModified(this.cell); + this._foldingIndicator = DOM.append(this.propertyHeaderContainer, DOM.$('.property-folding-indicator')); + this._foldingIndicator.classList.add(this.accessor.prefix); + this._updateFoldingIcon(); + const metadataStatus = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-status')); + this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); + + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + this.propertyHeaderContainer.classList.add('modified'); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + } + + const cellToolbarContainer = DOM.append(this.propertyHeaderContainer, DOM.$('div.property-toolbar')); + this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); + return item; + } + + return undefined; + } + }); + this._register(this._toolbar); + this._toolbar.context = { + cell: this.cell + }; + + const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer); + this._register(scopedContextKeyService); + const propertyChanged = NOTEBOOK_DIFF_CELL_PROPERTY.bindTo(scopedContextKeyService); + propertyChanged.set(metadataChanged); + this._propertyExpanded = NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED.bindTo(scopedContextKeyService); + + this._menu = this.menuService.createMenu(this.accessor.menuId, scopedContextKeyService); + this._register(this._menu); + + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + + this._register(this._menu.onDidChange(() => { + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + })); + + this._register(this.notebookEditor.onMouseUp(e => { + if (!e.event.target) { + return; + } + + const target = e.event.target as HTMLElement; + + if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) { + const parent = target.parentElement as HTMLElement; + + if (!parent) { + return; + } + + if (!parent.classList.contains(this.accessor.prefix)) { + return; + } + + if (!parent.classList.contains('property-folding-indicator')) { + return; + } + + // folding icon + + const cellViewModel = e.target; + + if (cellViewModel === this.cell) { + const oldFoldingState = this.accessor.getFoldingState(this.cell); + this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(this.cell.renderOutput); + } + } + + return; + })); + + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(this.cell.renderOutput); + } + + refresh() { + let metadataChanged = this.accessor.checkIfModified(this.cell); + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + this.propertyHeaderContainer.classList.add('modified'); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, undefined, actions); + this._toolbar.setActions(actions); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + this._statusSpan.style.fontWeight = 'normal'; + this._toolbar.setActions([]); + } + } + + private _updateFoldingIcon(): any { + if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon)); + this._propertyExpanded?.set(false); + } else { + DOM.reset(this._foldingIndicator, renderIcon(expandedIcon)); + this._propertyExpanded?.set(true); + } + + } +} + +abstract class AbstractElementRenderer extends Disposable { + protected _metadataHeaderContainer!: HTMLElement; + protected _metadataHeader!: PropertyHeader; + protected _metadataInfoContainer!: HTMLElement; + protected _metadataEditorContainer?: HTMLElement; + protected _metadataEditorDisposeStore!: DisposableStore; + protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; + + protected _outputHeaderContainer!: HTMLElement; + protected _outputHeader!: PropertyHeader; + protected _outputInfoContainer!: HTMLElement; + protected _outputEditorContainer?: HTMLElement; + protected _outputViewContainer?: HTMLElement; + protected _outputLeftContainer?: HTMLElement; + protected _outputRightContainer?: HTMLElement; + protected _outputEmptyElement?: HTMLElement; + protected _outputLeftView?: OutputContainer; + protected _outputRightView?: OutputContainer; + protected _outputEditorDisposeStore!: DisposableStore; + protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; + + + protected _diffEditorContainer!: HTMLElement; + protected _diagonalFill?: HTMLElement; + protected _isDisposed: boolean; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: DiffElementViewModelBase, + readonly templateData: CellDiffSingleSideRenderTemplate | CellDiffSideBySideRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + protected readonly textModelService: ITextModelService, + protected readonly contextMenuService: IContextMenuService, + protected readonly keybindingService: IKeybindingService, + protected readonly notificationService: INotificationService, + protected readonly menuService: IMenuService, + protected readonly contextKeyService: IContextKeyService + + + ) { + super(); + // init + this._isDisposed = false; + this._metadataEditorDisposeStore = new DisposableStore(); + this._outputEditorDisposeStore = new DisposableStore(); + this._register(this._metadataEditorDisposeStore); + this._register(this._outputEditorDisposeStore); + this._register(cell.onDidLayoutChange(e => this.layout(e))); + this._register(cell.onDidLayoutChange(e => this.updateBorders())); + this.buildBody(); + + this._register(cell.onDidStateChange(() => { + this.updateOutputRendering(this.cell.renderOutput); + })); + } + + abstract buildBody(): void; + + updateMetadataRendering() { + if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + // we should expand the metadata editor + this._metadataInfoContainer.style.display = 'block'; + + if (!this._metadataEditorContainer || !this._metadataEditor) { + // create editor + this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); + this._buildMetadataEditor(); + } else { + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + } + } else { + // we should collapse the metadata editor + this._metadataInfoContainer.style.display = 'none'; + // this._metadataEditorDisposeStore.clear(); + this.cell.metadataHeight = 0; + } + } + + updateOutputRendering(renderRichOutput: boolean) { + if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._outputInfoContainer.style.display = 'block'; + if (renderRichOutput) { + this._hideOutputsRaw(); + this._buildOutputRendererContainer(); + this._showOutputsRenderer(); + this._showOutputsEmptyView(); + } else { + this._hideOutputsRenderer(); + this._buildOutputRawContainer(); + this._showOutputsRaw(); + } + } else { + this._outputInfoContainer.style.display = 'none'; + + this._hideOutputsRaw(); + this._hideOutputsRenderer(); + this._hideOutputsEmptyView(); + } + } + + private _buildOutputRawContainer() { + if (!this._outputEditorContainer) { + this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); + this._buildOutputEditor(); + } + } + + private _showOutputsRaw() { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.display = 'block'; + this.cell.rawOutputHeight = this._outputEditor!.getContentHeight(); + } + } + + private _showOutputsEmptyView() { + this.cell.layoutChange(); + } + + private _hideOutputsRaw() { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.display = 'none'; + this.cell.rawOutputHeight = 0; + } + } + + private _hideOutputsEmptyView() { + this.cell.layoutChange(); + } + + abstract _buildOutputRendererContainer(): void; + abstract _hideOutputsRenderer(): void; + abstract _showOutputsRenderer(): void; + + private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { + let result: { [key: string]: any } = {}; + let newLangauge: string | undefined = undefined; + try { + const newMetadataObj = JSON.parse(newMetadata); + const keys = new Set([...Object.keys(newMetadataObj)]); + for (let key of keys) { + switch (key as keyof NotebookCellMetadata) { + case 'breakpointMargin': + case 'editable': + case 'hasExecutionOrder': + case 'inputCollapsed': + case 'outputCollapsed': + case 'runnable': + // boolean + if (typeof newMetadataObj[key] === 'boolean') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + + case 'executionOrder': + case 'lastRunDuration': + // number + if (typeof newMetadataObj[key] === 'number') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'runState': + // enum + if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'statusMessage': + // string + if (typeof newMetadataObj[key] === 'string') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + default: + if (key === 'language') { + newLangauge = newMetadataObj[key]; + } + result[key] = newMetadataObj[key]; + break; + } + } + + if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); + this.notebookEditor.textModel!.applyEdits( + this.notebookEditor.textModel!.versionId, + [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], + true, + undefined, + () => undefined, + undefined + ); + } + + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!.textModel); + + if (index < 0) { + return; + } + + this.notebookEditor.textModel!.applyEdits(this.notebookEditor.textModel!.versionId, [ + { editType: CellEditType.Metadata, index, metadata: result } + ], true, undefined, () => undefined, undefined); + } catch { + } + } + + private async _buildMetadataEditor() { + this._metadataEditorDisposeStore.clear(); + + if (this.cell instanceof SideBySideDiffElementViewModel) { + this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false, + originalEditable: false, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: this.cell.layoutInfo.metadataHeight, + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), true, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this.layout({ metadataHeight: true }); + this._metadataEditorDisposeStore.add(this._metadataEditor); + + this._metadataEditorContainer?.classList.add('diff'); + + const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.originalDocument.uri, this.cell.original!.handle)); + const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellMetadataUri(this.cell.modifiedDocument.uri, this.cell.modified!.handle)); + this._metadataEditor.setModel({ + original: originalMetadataModel.object.textEditorModel, + modified: modifiedMetadataModel.object.textEditorModel + }); + + this._metadataEditorDisposeStore.add(originalMetadataModel); + this._metadataEditorDisposeStore.add(modifiedMetadataModel); + + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + + this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this.cell.metadataHeight = e.contentHeight; + } + })); + + let respondingToContentChange = false; + + this._metadataEditorDisposeStore.add(modifiedMetadataModel.object.textEditorModel.onDidChangeContent(() => { + respondingToContentChange = true; + const value = modifiedMetadataModel.object.textEditorModel.getValue(); + this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); + this._metadataHeader.refresh(); + respondingToContentChange = false; + })); + + this._metadataEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeMetadata(() => { + if (respondingToContentChange) { + return; + } + + const modifiedMetadataSource = getFormatedMetadataJSON(this.notebookEditor.textModel!, this.cell.modified?.metadata || {}, this.cell.modified?.language); + modifiedMetadataModel.object.textEditorModel.setValue(modifiedMetadataSource); + })); + + return; + } else { + this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + this.layout({ metadataHeight: true }); + this._metadataEditorDisposeStore.add(this._metadataEditor); + + const mode = this.modeService.create('jsonc'); + const originalMetadataSource = getFormatedMetadataJSON(this.notebookEditor.textModel!, + this.cell.type === 'insert' + ? this.cell.modified!.metadata || {} + : this.cell.original!.metadata || {}); + const uri = this.cell.type === 'insert' + ? this.cell.modified!.uri + : this.cell.original!.uri; + const handle = this.cell.type === 'insert' + ? this.cell.modified!.handle + : this.cell.original!.handle; + + const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); + this._metadataEditor.setModel(metadataModel); + this._metadataEditorDisposeStore.add(metadataModel); + + this.cell.metadataHeight = this._metadataEditor.getContentHeight(); + + this._metadataEditorDisposeStore.add(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this.cell.metadataHeight = e.contentHeight; + } + })); + } + } + + private _getFormatedOutputJSON(outputs: IProcessedOutput[]) { + return JSON.stringify(outputs, undefined, '\t'); + } + + private _buildOutputEditor() { + this._outputEditorDisposeStore.clear(); + + if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { + const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + if (originalOutputsSource !== modifiedOutputsSource) { + const mode = this.modeService.create('json'); + const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); + this._outputEditorDisposeStore.add(originalModel); + this._outputEditorDisposeStore.add(modifiedModel); + + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const lineCount = Math.max(originalModel.getLineCount(), modifiedModel.getLineCount()); + this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.layoutInfo.rawOutputHeight || lineHeight * lineCount), + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this._outputEditorDisposeStore.add(this._outputEditor); + + this._outputEditorContainer?.classList.add('diff'); + + this._outputEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState() as editorCommon.IDiffEditorViewState); + + this.cell.rawOutputHeight = this._outputEditor.getContentHeight(); + + this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this.cell.rawOutputHeight = e.contentHeight; + } + })); + + this._outputEditorDisposeStore.add(this.cell.modified!.textModel.onDidChangeOutputs(() => { + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + modifiedModel.setValue(modifiedOutputsSource); + this._outputHeader.refresh(); + })); + + return; + } + } + + this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, this.cell.type === 'unchanged' || this.cell.type === 'modified') - 32), + height: this.cell.layoutInfo.rawOutputHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + this._outputEditorDisposeStore.add(this._outputEditor); + + const mode = this.modeService.create('json'); + const originaloutputSource = this._getFormatedOutputJSON( + this.notebookEditor.textModel!.transientOptions.transientOutputs + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); + const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); + this._outputEditorDisposeStore.add(outputModel); + this._outputEditor.setModel(outputModel); + this._outputEditor.restoreViewState(this.cell.getOutputEditorViewState()); + + this.cell.rawOutputHeight = this._outputEditor.getContentHeight(); + + this._outputEditorDisposeStore.add(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this.cell.rawOutputHeight = e.contentHeight; + } + })); + } + + protected layoutNotebookCell() { + this.notebookEditor.layoutNotebookCell( + this.cell, + this.cell.layoutInfo.totalHeight + ); + } + + updateBorders() { + this.templateData.leftBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this.templateData.rightBorder.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + this.templateData.bottomBorder.style.top = `${this.cell.layoutInfo.totalHeight - 32}px`; + } + + dispose() { + if (this._outputEditor) { + this.cell.saveOutputEditorViewState(this._outputEditor.saveViewState()); + } + + if (this._metadataEditor) { + this.cell.saveMetadataEditorViewState(this._metadataEditor.saveViewState()); + } + + this._metadataEditorDisposeStore.dispose(); + this._outputEditorDisposeStore.dispose(); + + this._isDisposed = true; + super.dispose(); + } + + abstract styleContainer(container: HTMLElement): void; + abstract updateSourceEditor(): void; + abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, metadataHeight?: boolean, outputEditor?: boolean, outputView?: boolean }): void; +} + +abstract class SingleSideDiffElement extends AbstractElementRenderer { + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + protected readonly textModelService: ITextModelService, + protected readonly contextMenuService: IContextMenuService, + protected readonly keybindingService: IKeybindingService, + protected readonly notificationService: INotificationService, + protected readonly menuService: IMenuService, + protected readonly contextKeyService: IContextKeyService + + + ) { + super( + notebookEditor, + cell, + templateData, + style, + instantiationService, + modeService, + modelService, + textModelService, + contextMenuService, + keybindingService, + notificationService, + menuService, + contextKeyService + ); + } + + buildBody() { + const body = this.templateData.body; + this._diffEditorContainer = this.templateData.diffEditorContainer; + switch (this.style) { + case 'left': + body.classList.add('left'); + break; + case 'right': + body.classList.add('right'); + break; + default: + body.classList.add('full'); + break; + } + + this._diagonalFill = this.templateData.diagonalFill; + this.styleContainer(this._diffEditorContainer); + this.updateSourceEditor(); + + this._metadataHeaderContainer = this.templateData.metadataHeaderContainer; + this._metadataInfoContainer = this.templateData.metadataInfoContainer; + this._metadataHeaderContainer.innerText = ''; + this._metadataInfoContainer.innerText = ''; + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkMetadataIfModified(); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { + this.cell.rawOutputHeight = 0; + this.cell.outputStatusHeight = 0; + this.templateData.outputHeaderContainer.style.display = 'none'; + this.templateData.outputInfoContainer.style.display = 'none'; + return; + } else { + this.templateData.outputHeaderContainer.style.display = 'flex'; + this.templateData.outputInfoContainer.style.display = 'block'; + + } + + this._outputHeaderContainer = this.templateData.outputHeaderContainer; + this._outputInfoContainer = this.templateData.outputInfoContainer; + + this._outputHeaderContainer.innerText = ''; + this._outputInfoContainer.innerText = ''; + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkIfOutputsModified(); + }, + getFoldingState: (cell) => { + return cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } +} +export class DeletedElement extends SingleSideDiffElement { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + + + ) { + super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement) { + container.classList.remove('inserted'); + container.classList.add('removed'); + } + + updateSourceEditor(): void { + const originalCell = this.cell.original!; + const lineCount = originalCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + this._editor = this.templateData.sourceEditor; + this._editor.layout({ + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }); + + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + originalCell.textModel.resolveTextModelRef().then(ref => { + if (this._isDisposed) { + return; + } + + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this.cell.editorHeight = this._editor.getContentHeight(); + }); + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.metadataHeight || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.metadataHeight + }); + } + + if (state.outputTotalHeight || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.outputTotalHeight + }); + } + + if (this._diagonalFill) { + this._diagonalFill.style.height = `${this.cell.layoutInfo.totalHeight - 32}px`; + } + + this.layoutNotebookCell(); + }); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + const span = DOM.append(this._outputEmptyElement, DOM.$('span')); + span.innerText = 'No outputs to render'; + + if (this.cell.original!.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputViewContainer!); + this._register(this._outputLeftView); + this._outputLeftView.render(); + + const removedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.original!.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []); + removedOutputRenderListener.dispose(); + } + }); + + this._register(removedOutputRenderListener); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original!.id, ['nb-cellDeleted'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + + this._outputLeftView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + + this._outputLeftView?.hideOutputs(); + } + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} + +export class InsertElement extends SingleSideDiffElement { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SingleSideDiffElementViewModel, + readonly templateData: CellDiffSingleSideRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + ) { + super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement): void { + container.classList.remove('removed'); + container.classList.add('inserted'); + } + + updateSourceEditor(): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + + this._editor = this.templateData.sourceEditor; + this._editor.layout( + { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + } + ); + this._editor.updateOptions({ readOnly: false }); + this.cell.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + modifiedCell.textModel.resolveTextModelRef().then(ref => { + if (this._isDisposed) { + return; + } + + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._editor.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.ICodeEditorViewState); + this.cell.editorHeight = this._editor.getContentHeight(); + }); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + this._outputEmptyElement.innerText = 'No outputs to render'; + + if (this.cell.modified!.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputViewContainer!); + this._register(this._outputRightView); + this._outputRightView.render(); + + const insertOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.modified!.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []); + insertOutputRenderListener.dispose(); + } + }); + this._register(insertOutputRenderListener); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified!.id, ['nb-cellAdded'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + this._outputRightView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + this._outputRightView?.hideOutputs(); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.metadataHeight || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this.cell.layoutInfo.metadataHeight + }); + } + + if (state.outputTotalHeight || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this.cell.layoutInfo.outputTotalHeight + }); + } + + this.layoutNotebookCell(); + + if (this._diagonalFill) { + this._diagonalFill.style.height = `${this.cell.layoutInfo.editorHeight + this.cell.layoutInfo.editorMargin + this.cell.layoutInfo.metadataStatusHeight + this.cell.layoutInfo.metadataHeight + this.cell.layoutInfo.outputTotalHeight + this.cell.layoutInfo.outputStatusHeight}px`; + } + }); + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} + +export class ModifiedElement extends AbstractElementRenderer { + private _editor?: DiffEditorWidget; + private _editorContainer!: HTMLElement; + private _inputToolbarContainer!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: SideBySideDiffElementViewModel, + readonly templateData: CellDiffSideBySideRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @ITextModelService readonly textModelService: ITextModelService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @INotificationService protected readonly notificationService: INotificationService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService + ) { + super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService, textModelService, contextMenuService, keybindingService, notificationService, menuService, contextKeyService); + } + + styleContainer(container: HTMLElement): void { + container.classList.remove('inserted', 'removed'); + } + + buildBody() { + const body = this.templateData.body; + this._diffEditorContainer = this.templateData.diffEditorContainer; + body.classList.remove('left', 'right', 'full'); + switch (this.style) { + case 'left': + body.classList.add('left'); + break; + case 'right': + body.classList.add('right'); + break; + default: + body.classList.add('full'); + break; + } + + this.styleContainer(this._diffEditorContainer); + this.updateSourceEditor(); + + this._metadataHeaderContainer = this.templateData.metadataHeaderContainer; + this._metadataInfoContainer = this.templateData.metadataInfoContainer; + + this._metadataHeaderContainer.innerText = ''; + this._metadataInfoContainer.innerText = ''; + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkMetadataIfModified(); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + if (this.notebookEditor.textModel?.transientOptions.transientOutputs) { + this.cell.rawOutputHeight = 0; + this.cell.outputStatusHeight = 0; + this.templateData.outputHeaderContainer.style.display = 'none'; + this.templateData.outputInfoContainer.style.display = 'none'; + return; + } else { + this.templateData.outputHeaderContainer.style.display = 'flex'; + this.templateData.outputInfoContainer.style.display = 'block'; + } + + this._outputHeaderContainer = this.templateData.outputHeaderContainer; + this._outputInfoContainer = this.templateData.outputInfoContainer; + this._outputHeaderContainer.innerText = ''; + this._outputInfoContainer.innerText = ''; + + if (this.cell.checkIfOutputsModified()) { + this._outputInfoContainer.classList.add('modified'); + } + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.checkIfOutputsModified(); + }, + getFoldingState: (cell) => { + return cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } + + _buildOutputRendererContainer() { + if (!this._outputViewContainer) { + this._outputViewContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-view-container')); + this._outputEmptyElement = DOM.append(this._outputViewContainer, DOM.$('.output-empty-view')); + this._outputEmptyElement.innerText = 'No outputs to render'; + + if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) { + this._outputEmptyElement.style.display = 'block'; + } else { + this._outputEmptyElement.style.display = 'none'; + } + + this.cell.layoutChange(); + + this._register(this.cell.modified.textModel.onDidChangeOutputs(() => { + // currently we only allow outputs change to the modified cell + if (!this.cell.checkIfOutputsModified() && this.cell.modified.outputs.length === 0) { + this._outputEmptyElement!.style.display = 'block'; + } else { + this._outputEmptyElement!.style.display = 'none'; + } + })); + + this._outputLeftContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-left')); + this._outputRightContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-right')); + // We should use the original text model here + this._outputLeftView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.original!, DiffSide.Original, this._outputLeftContainer!); + this._outputLeftView.render(); + this._register(this._outputLeftView); + this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputRightContainer!); + this._outputRightView.render(); + this._register(this._outputRightView); + + const originalOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.original.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); + originalOutputRenderListener.dispose(); + } + }); + + const modifiedOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { + if (e.cell.uri.toString() === this.cell.modified.uri.toString()) { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []); + modifiedOutputRenderListener.dispose(); + } + }); + + this._register(originalOutputRenderListener); + this._register(modifiedOutputRenderListener); + + this._decorate(); + } + + this._outputViewContainer.style.display = 'block'; + } + + _decorate() { + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); + this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Modified, this.cell.modified.id, ['nb-cellAdded'], []); + } + + _showOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'block'; + + this._outputLeftView?.showOutputs(); + this._outputRightView?.showOutputs(); + this._decorate(); + } + } + + _hideOutputsRenderer() { + if (this._outputViewContainer) { + this._outputViewContainer.style.display = 'none'; + + this._outputLeftView?.hideOutputs(); + this._outputRightView?.hideOutputs(); + } + } + + updateSourceEditor(): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : lineCount * lineHeight + getEditorTopPadding() + EDITOR_BOTTOM_PADDING; + this._editorContainer = this.templateData.editorContainer; + this._editor = this.templateData.sourceEditor; + + this._editorContainer.classList.add('diff'); + + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + + this._editorContainer.style.height = `${editorHeight}px`; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.layoutInfo.editorHeight !== e.contentHeight) { + this.cell.editorHeight = e.contentHeight; + } + })); + + this._initializeSourceDiffEditor(); + + this._inputToolbarContainer = this.templateData.inputToolbarContainer; + this._toolbar = this.templateData.toolbar; + + this._toolbar.context = { + cell: this.cell + }; + + this._menu = this.menuService.createMenu(MenuId.NotebookDiffCellInputTitle, this.contextKeyService); + this._register(this._menu); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + + if (this.cell.modified!.textModel.getValue() !== this.cell.original!.textModel.getValue()) { + this._inputToolbarContainer.style.display = 'block'; + } else { + this._inputToolbarContainer.style.display = 'none'; + } + + this._register(this.cell.modified!.textModel.onDidChangeContent(() => { + if (this.cell.modified!.textModel.getValue() !== this.cell.original!.textModel.getValue()) { + this._inputToolbarContainer.style.display = 'block'; + } else { + this._inputToolbarContainer.style.display = 'none'; + } + })); + } + + private async _initializeSourceDiffEditor() { + const originalCell = this.cell.original!; + const modifiedCell = this.cell.modified!; + + const originalRef = await originalCell.textModel.resolveTextModelRef(); + const modifiedRef = await modifiedCell.textModel.resolveTextModelRef(); + + if (this._isDisposed) { + return; + } + + const textModel = originalRef.object.textEditorModel; + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register({ + dispose: () => { + const delayer = new Delayer(5000); + delayer.trigger(() => { + originalRef.dispose(); + delayer.dispose(); + }); + } + }); + this._register({ + dispose: () => { + const delayer = new Delayer(5000); + delayer.trigger(() => { + modifiedRef.dispose(); + delayer.dispose(); + }); + } + }); + + this._editor!.setModel({ + original: textModel, + modified: modifiedTextModel + }); + + this._editor!.restoreViewState(this.cell.getSourceEditorViewState() as editorCommon.IDiffEditorViewState); + + const contentHeight = this._editor!.getContentHeight(); + this.cell.editorHeight = contentHeight; + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataHeight?: boolean, outputTotalHeight?: boolean }) { + DOM.scheduleAtNextAnimationFrame(() => { + if (state.editorHeight) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor!.layout({ + width: this._editor!.getViewWidth(), + height: this.cell.layoutInfo.editorHeight + }); + } + + if (state.outerWidth) { + this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; + this._editor!.layout(); + } + + if (state.metadataHeight || state.outerWidth) { + if (this._metadataEditorContainer) { + this._metadataEditorContainer.style.height = `${this.cell.layoutInfo.metadataHeight}px`; + this._metadataEditor?.layout(); + } + } + + if (state.outputTotalHeight || state.outerWidth) { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.height = `${this.cell.layoutInfo.outputTotalHeight}px`; + this._outputEditor?.layout(); + } + } + + + this.layoutNotebookCell(); + }); + } + + dispose() { + if (this._editor) { + this.cell.saveSpirceEditorViewState(this._editor.saveViewState()); + } + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts new file mode 100644 index 0000000000..5f45051bc7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -0,0 +1,361 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffSide, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { ICellOutputViewModel, IDisplayOutputViewModel, IRenderOutput, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { BUILTIN_RENDERER_ID, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; + +interface IMimeTypeRenderer extends IQuickPickItem { + index: number; +} + +export class OutputElement extends Disposable { + readonly resizeListener = new DisposableStore(); + domNode!: HTMLElement; + renderResult?: IRenderOutput; + + constructor( + private _notebookEditor: INotebookTextDiffEditor, + private _notebookTextModel: NotebookTextModel, + private _notebookService: INotebookService, + private _quickInputService: IQuickInputService, + private _diffElementViewModel: DiffElementViewModelBase, + private _diffSide: DiffSide, + private _nestedCell: DiffNestedCellViewModel, + private _outputContainer: HTMLElement, + readonly output: ICellOutputViewModel + ) { + super(); + } + + render(index: number, beforeElement?: HTMLElement) { + const outputItemDiv = document.createElement('div'); + let result: IRenderOutput | undefined = undefined; + + if (this.output.isDisplayOutput()) { + const [mimeTypes, pick] = this.output.resolveMimeTypes(this._notebookTextModel); + const pickedMimeTypeRenderer = mimeTypes[pick]; + if (mimeTypes.length > 1) { + outputItemDiv.style.position = 'relative'; + const mimeTypePicker = DOM.$('.multi-mimetype-output'); + mimeTypePicker.classList.add(...ThemeIcon.asClassNameArray(mimetypeIcon)); + mimeTypePicker.tabIndex = 0; + mimeTypePicker.title = nls.localize('mimeTypePicker', "Choose a different output mimetype, available mimetypes: {0}", mimeTypes.map(mimeType => mimeType.mimeType).join(', ')); + outputItemDiv.appendChild(mimeTypePicker); + this.resizeListener.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => { + if (e.leftButton) { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output as IDisplayOutputViewModel); + } + })); + + this.resizeListener.add((DOM.addDisposableListener(mimeTypePicker, DOM.EventType.KEY_DOWN, async e => { + const event = new StandardKeyboardEvent(e); + if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output as IDisplayOutputViewModel); + } + }))); + } + + const innerContainer = DOM.$('.output-inner-container'); + DOM.append(outputItemDiv, innerContainer); + + + if (pickedMimeTypeRenderer.rendererId !== BUILTIN_RENDERER_ID) { + const renderer = this._notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); + result = renderer + ? { type: RenderOutputType.Extension, renderer, source: this.output, mimeType: pickedMimeTypeRenderer.mimeType } + : this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri,); + } else { + result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, pickedMimeTypeRenderer.mimeType, this._notebookTextModel.uri); + } + + this.output.pickedMimeType = pick; + } else { + // for text and error, there is no mimetype + const innerContainer = DOM.$('.output-inner-container'); + DOM.append(outputItemDiv, innerContainer); + + result = this._notebookEditor.getOutputRenderer().render(this.output, innerContainer, undefined, this._notebookTextModel.uri); + } + + this.domNode = outputItemDiv; + this.renderResult = result; + + if (!result) { + // this.viewCell.updateOutputHeight(index, 0); + return; + } + + if (beforeElement) { + this._outputContainer.insertBefore(outputItemDiv, beforeElement); + } else { + this._outputContainer.appendChild(outputItemDiv); + } + + if (result.type !== RenderOutputType.None) { + // this.viewCell.selfSizeMonitoring = true; + this._notebookEditor.createInset( + this._diffElementViewModel, + this._nestedCell, + result, + () => this.getOutputOffsetInCell(index), + this._diffElementViewModel instanceof SideBySideDiffElementViewModel + ? this._diffSide + : this._diffElementViewModel.type === 'insert' ? DiffSide.Modified : DiffSide.Original + ); + } else { + outputItemDiv.classList.add('foreground', 'output-element'); + outputItemDiv.style.position = 'absolute'; + } + + if (outputHasDynamicHeight(result)) { + // this.viewCell.selfSizeMonitoring = true; + const clientHeight = outputItemDiv.clientHeight; + // TODO, set an inital dimension to avoid force reflow + // const dimension = { + // width: this.cellViewModel., + // height: clientHeight + // }; + + const elementSizeObserver = getResizesObserver(outputItemDiv, undefined, () => { + if (this._outputContainer && document.body.contains(this._outputContainer)) { + const height = Math.ceil(elementSizeObserver.getHeight()); + + if (clientHeight === height) { + return; + } + + const currIndex = this.getCellOutputCurrentIndex(); + if (currIndex < 0) { + return; + } + + this.updateHeight(currIndex, height); + } + }); + elementSizeObserver.startObserving(); + this.resizeListener.add(elementSizeObserver); + this.updateHeight(index, clientHeight); + } else if (result.type === RenderOutputType.None) { // no-op if it's a webview + const clientHeight = Math.ceil(outputItemDiv.clientHeight); + this.updateHeight(index, clientHeight); + + const top = this.getOutputOffsetInContainer(index); + outputItemDiv.style.top = `${top}px`; + } + } + + private async pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, viewModel: IDisplayOutputViewModel) { + const [mimeTypes, currIndex] = viewModel.resolveMimeTypes(notebookTextModel); + + const items = mimeTypes.filter(mimeType => mimeType.isTrusted).map((mimeType, index): IMimeTypeRenderer => ({ + label: mimeType.mimeType, + id: mimeType.mimeType, + index: index, + picked: index === currIndex, + detail: this.generateRendererInfo(mimeType.rendererId), + description: index === currIndex ? nls.localize('curruentActiveMimeType', "Currently Active") : undefined + })); + + const picker = this._quickInputService.createQuickPick(); + picker.items = items; + picker.activeItems = items.filter(item => !!item.picked); + picker.placeholder = items.length !== mimeTypes.length + ? nls.localize('promptChooseMimeTypeInSecure.placeHolder', "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted") + : nls.localize('promptChooseMimeType.placeHolder', "Select mimetype to render for current output"); + + const pick = await new Promise(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (pick === undefined) { + return; + } + + if (pick !== currIndex) { + // user chooses another mimetype + const index = this._nestedCell.outputsViewModels.indexOf(viewModel); + const nextElement = this.domNode.nextElementSibling; + this.resizeListener.clear(); + const element = this.domNode; + if (element) { + element.parentElement?.removeChild(element); + this._notebookEditor.removeInset( + this._diffElementViewModel, + this._nestedCell, + viewModel, + this._diffSide + ); + } + + viewModel.pickedMimeType = pick; + this.render(index, nextElement as HTMLElement); + } + } + + private generateRendererInfo(renderId: string | undefined): string { + if (renderId === undefined || renderId === BUILTIN_RENDERER_ID) { + return nls.localize('builtinRenderInfo', "built-in"); + } + + const renderInfo = this._notebookService.getRendererInfo(renderId); + + if (renderInfo) { + const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id; + return `${displayName} (${renderInfo.extensionId.value})`; + } + + return nls.localize('builtinRenderInfo', "built-in"); + } + + getCellOutputCurrentIndex() { + return this._diffElementViewModel.getNestedCellViewModel(this._diffSide).outputs.indexOf(this.output.model); + } + + updateHeight(index: number, height: number) { + this._diffElementViewModel.updateOutputHeight(this._diffSide, index, height); + } + + getOutputOffsetInContainer(index: number) { + return this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index); + } + + getOutputOffsetInCell(index: number) { + return this._diffElementViewModel.getOutputOffsetInCell(this._diffSide, index); + } +} + +export class OutputContainer extends Disposable { + private _outputEntries = new Map(); + constructor( + private _editor: INotebookTextDiffEditor, + private _notebookTextModel: NotebookTextModel, + private _diffElementViewModel: DiffElementViewModelBase, + private _nestedCellViewModel: DiffNestedCellViewModel, + private _diffSide: DiffSide, + private _outputContainer: HTMLElement, + @INotebookService private _notebookService: INotebookService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IOpenerService readonly _openerService: IOpenerService, + @ITextFileService readonly _textFileService: ITextFileService, + + ) { + super(); + this._register(this._diffElementViewModel.onDidLayoutChange(() => { + this._outputEntries.forEach((value, key) => { + const index = _nestedCellViewModel.outputs.indexOf(key.model); + if (index >= 0) { + const top = this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index); + value.domNode.style.top = `${top}px`; + } + }); + })); + + this._register(this._nestedCellViewModel.textModel.onDidChangeOutputs(splices => { + this._updateOutputs(splices); + })); + } + + private _updateOutputs(splices: NotebookCellOutputsSplice[]) { + if (!splices.length) { + return; + } + + const removedKeys: ICellOutputViewModel[] = []; + + this._outputEntries.forEach((value, key) => { + if (this._nestedCellViewModel.outputsViewModels.indexOf(key) < 0) { + // already removed + removedKeys.push(key); + // remove element from DOM + this._outputContainer.removeChild(value.domNode); + if (key.isDisplayOutput()) { + this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide); + } + } + }); + + removedKeys.forEach(key => { + this._outputEntries.get(key)?.dispose(); + this._outputEntries.delete(key); + }); + + let prevElement: HTMLElement | undefined = undefined; + const outputsToRender = this._nestedCellViewModel.outputsViewModels; + + outputsToRender.reverse().forEach(output => { + if (this._outputEntries.has(output)) { + // already exist + prevElement = this._outputEntries.get(output)!.domNode; + return; + } + + // newly added element + const currIndex = this._nestedCellViewModel.outputsViewModels.indexOf(output); + this._renderOutput(output, currIndex, prevElement); + prevElement = this._outputEntries.get(output)?.domNode; + }); + } + render() { + // TODO, outputs to render (should have a limit) + for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) { + const currOutput = this._nestedCellViewModel.outputsViewModels[index]; + + // always add to the end + this._renderOutput(currOutput, index, undefined); + } + } + + showOutputs() { + for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) { + const currOutput = this._nestedCellViewModel.outputsViewModels[index]; + + if (currOutput.isDisplayOutput()) { + // always add to the end + this._editor.showInset(this._diffElementViewModel, currOutput.cellViewModel, currOutput, this._diffSide); + } + } + } + + hideOutputs() { + this._outputEntries.forEach((outputElement, cellOutputViewModel) => { + if (cellOutputViewModel.isDisplayOutput()) { + this._editor.hideInset(this._diffElementViewModel, this._nestedCellViewModel, cellOutputViewModel); + } + }); + } + + private _renderOutput(currOutput: ICellOutputViewModel, index: number, beforeElement?: HTMLElement) { + if (!this._outputEntries.has(currOutput)) { + this._outputEntries.set(currOutput, new OutputElement(this._editor, this._notebookTextModel, this._notebookService, this._quickInputService, this._diffElementViewModel, this._diffSide, this._nestedCellViewModel, this._outputContainer, currOutput)); + } + + const renderElement = this._outputEntries.get(currOutput)!; + renderElement.render(index, beforeElement); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts new file mode 100644 index 0000000000..aa9a1ca379 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -0,0 +1,498 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellDiffViewModelLayoutChangeEvent, DiffSide, DIFF_CELL_MARGIN, IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { IGenericCellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { hash } from 'vs/base/common/hash'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { URI } from 'vs/base/common/uri'; +import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; +import * as editorCommon from 'vs/editor/common/editorCommon'; + +export enum PropertyFoldingState { + Expanded, + Collapsed +} + +export const OUTPUT_EDITOR_HEIGHT_MAGIC = 1440; + +type ILayoutInfoDelta0 = { [K in keyof IDiffElementLayoutInfo]?: number; }; +interface ILayoutInfoDelta extends ILayoutInfoDelta0 { + rawOutputHeight?: number; + recomputeOutput?: boolean; +} + +export abstract class DiffElementViewModelBase extends Disposable { + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; + protected _layoutInfoEmitter = new Emitter(); + onDidLayoutChange = this._layoutInfoEmitter.event; + protected _stateChangeEmitter = new Emitter<{ renderOutput: boolean; }>(); + onDidStateChange = this._stateChangeEmitter.event; + protected _layoutInfo!: IDiffElementLayoutInfo; + + set rawOutputHeight(height: number) { + this._layout({ rawOutputHeight: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, height) }); + } + + get rawOutputHeight() { + throw new Error('Use Cell.layoutInfo.rawOutputHeight'); + } + + set outputStatusHeight(height: number) { + this._layout({ outputStatusHeight: height }); + } + + get outputStatusHeight() { + throw new Error('Use Cell.layoutInfo.outputStatusHeight'); + } + + set editorHeight(height: number) { + this._layout({ editorHeight: height }); + } + + get editorHeight() { + throw new Error('Use Cell.layoutInfo.editorHeight'); + } + + set editorMargin(margin: number) { + this._layout({ editorMargin: margin }); + } + + get editorMargin() { + throw new Error('Use Cell.layoutInfo.editorMargin'); + } + + set metadataHeight(height: number) { + this._layout({ metadataHeight: height }); + } + + get metadataHeight() { + throw new Error('Use Cell.layoutInfo.metadataHeight'); + } + + private _renderOutput = true; + + set renderOutput(value: boolean) { + this._renderOutput = value; + this._layout({ recomputeOutput: true }); + this._stateChangeEmitter.fire({ renderOutput: this._renderOutput }); + } + + get renderOutput() { + return this._renderOutput; + } + + get layoutInfo(): IDiffElementLayoutInfo { + return this._layoutInfo; + } + + private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel | undefined, + readonly modified: DiffNestedCellViewModel | undefined, + readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(); + this._layoutInfo = { + width: 0, + editorHeight: 0, + editorMargin: 0, + metadataHeight: 0, + metadataStatusHeight: 25, + rawOutputHeight: 0, + outputTotalHeight: 0, + outputStatusHeight: 25, + bodyMargin: 32, + totalHeight: 82 + }; + + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => { + this._layoutInfoEmitter.fire({ outerWidth: true }); + })); + } + + layoutChange() { + this._layout({ recomputeOutput: true }); + } + + protected _layout(delta: ILayoutInfoDelta) { + const width = delta.width !== undefined ? delta.width : this._layoutInfo.width; + const editorHeight = delta.editorHeight !== undefined ? delta.editorHeight : this._layoutInfo.editorHeight; + const editorMargin = delta.editorMargin !== undefined ? delta.editorMargin : this._layoutInfo.editorMargin; + const metadataHeight = delta.metadataHeight !== undefined ? delta.metadataHeight : this._layoutInfo.metadataHeight; + const metadataStatusHeight = delta.metadataStatusHeight !== undefined ? delta.metadataStatusHeight : this._layoutInfo.metadataStatusHeight; + const rawOutputHeight = delta.rawOutputHeight !== undefined ? delta.rawOutputHeight : this._layoutInfo.rawOutputHeight; + const outputStatusHeight = delta.outputStatusHeight !== undefined ? delta.outputStatusHeight : this._layoutInfo.outputStatusHeight; + const bodyMargin = delta.bodyMargin !== undefined ? delta.bodyMargin : this._layoutInfo.bodyMargin; + const outputHeight = (delta.recomputeOutput || delta.rawOutputHeight !== undefined) ? this._getOutputTotalHeight(rawOutputHeight) : this._layoutInfo.outputTotalHeight; + + const totalHeight = editorHeight + + editorMargin + + metadataHeight + + metadataStatusHeight + + outputHeight + + outputStatusHeight + + bodyMargin; + + const newLayout: IDiffElementLayoutInfo = { + width: width, + editorHeight: editorHeight, + editorMargin: editorMargin, + metadataHeight: metadataHeight, + metadataStatusHeight: metadataStatusHeight, + outputTotalHeight: outputHeight, + outputStatusHeight: outputStatusHeight, + bodyMargin: bodyMargin, + rawOutputHeight: rawOutputHeight, + totalHeight: totalHeight + }; + + const changeEvent: CellDiffViewModelLayoutChangeEvent = {}; + + if (newLayout.width !== this._layoutInfo.width) { + changeEvent.width = true; + } + + if (newLayout.editorHeight !== this._layoutInfo.editorHeight) { + changeEvent.editorHeight = true; + } + + if (newLayout.editorMargin !== this._layoutInfo.editorMargin) { + changeEvent.editorMargin = true; + } + + if (newLayout.metadataHeight !== this._layoutInfo.metadataHeight) { + changeEvent.metadataHeight = true; + } + + if (newLayout.metadataStatusHeight !== this._layoutInfo.metadataStatusHeight) { + changeEvent.metadataStatusHeight = true; + } + + if (newLayout.outputTotalHeight !== this._layoutInfo.outputTotalHeight) { + changeEvent.outputTotalHeight = true; + } + + if (newLayout.outputStatusHeight !== this._layoutInfo.outputStatusHeight) { + changeEvent.outputStatusHeight = true; + } + + if (newLayout.bodyMargin !== this._layoutInfo.bodyMargin) { + changeEvent.bodyMargin = true; + } + + if (newLayout.totalHeight !== this._layoutInfo.totalHeight) { + changeEvent.totalHeight = true; + } + + this._layoutInfo = newLayout; + this._fireLayoutChangeEvent(changeEvent); + } + + private _getOutputTotalHeight(rawOutputHeight: number) { + if (this.outputFoldingState === PropertyFoldingState.Collapsed) { + return 0; + } + + if (this.renderOutput) { + if (this.isOutputEmpty()) { + // single line; + return 24; + } + return this.getRichOutputTotalHeight(); + } else { + return rawOutputHeight; + } + } + + private _fireLayoutChangeEvent(state: CellDiffViewModelLayoutChangeEvent) { + this._layoutInfoEmitter.fire(state); + this.editorEventDispatcher.emit([{ type: NotebookDiffViewEventType.CellLayoutChanged, source: this._layoutInfo }]); + } + + abstract checkIfOutputsModified(): boolean; + abstract checkMetadataIfModified(): boolean; + abstract isOutputEmpty(): boolean; + abstract getRichOutputTotalHeight(): number; + abstract getCellByUri(cellUri: URI): IGenericCellViewModel; + abstract getOutputOffsetInCell(diffSide: DiffSide, index: number): number; + abstract getOutputOffsetInContainer(diffSide: DiffSide, index: number): number; + abstract updateOutputHeight(diffSide: DiffSide, index: number, height: number): void; + abstract getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel; + + getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { + if (fullWidth) { + return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; + } + + return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; + } + + getOutputEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._outputEditorViewState; + } + + saveOutputEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._outputEditorViewState = viewState; + } + + getMetadataEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._metadataEditorViewState; + } + + saveMetadataEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._metadataEditorViewState = viewState; + } + + getSourceEditorViewState(): editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null { + return this._sourceEditorViewState; + } + + saveSpirceEditorViewState(viewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null) { + this._sourceEditorViewState = viewState; + } +} + +export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { + get originalDocument() { + return this.otherDocumentTextModel; + } + + get modifiedDocument() { + return this.mainDocumentTextModel; + } + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly otherDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel, + readonly modified: DiffNestedCellViewModel, + readonly type: 'unchanged' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super( + mainDocumentTextModel, + original, + modified, + type, + editorEventDispatcher); + + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + if (this.checkMetadataIfModified()) { + this.metadataFoldingState = PropertyFoldingState.Expanded; + } + + if (this.checkIfOutputsModified()) { + this.outputFoldingState = PropertyFoldingState.Expanded; + } + + this._register(this.original.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + + this._register(this.modified.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + } + + checkIfOutputsModified() { + return !this.mainDocumentTextModel.transientOptions.transientOutputs && hash(this.original?.outputs ?? []) !== hash(this.modified?.outputs ?? []); + } + + checkMetadataIfModified(): boolean { + return hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.original?.metadata || {}, this.original?.language)) !== hash(getFormatedMetadataJSON(this.mainDocumentTextModel, this.modified?.metadata ?? {}, this.modified?.language)); + } + + updateOutputHeight(diffSide: DiffSide, index: number, height: number) { + if (diffSide === DiffSide.Original) { + this.original.updateOutputHeight(index, height); + } else { + this.modified.updateOutputHeight(index, height); + } + } + + getOutputOffsetInContainer(diffSide: DiffSide, index: number) { + if (diffSide === DiffSide.Original) { + return this.original.getOutputOffset(index); + } else { + return this.modified.getOutputOffset(index); + } + } + + getOutputOffsetInCell(diffSide: DiffSide, index: number) { + const offsetInOutputsContainer = this.getOutputOffsetInContainer(diffSide, index); + + return this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin / 2 + + offsetInOutputsContainer; + } + + isOutputEmpty() { + if (this.mainDocumentTextModel.transientOptions.transientOutputs) { + return true; + } + + if (this.checkIfOutputsModified()) { + return false; + } + + // outputs are not changed + + return (this.original?.outputs || []).length === 0; + } + + getRichOutputTotalHeight() { + return Math.max(this.original.getOutputTotalHeight(), this.modified.getOutputTotalHeight()); + } + + getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel { + throw new Error('Method not implemented.'); + } + + getCellByUri(cellUri: URI): IGenericCellViewModel { + if (cellUri.toString() === this.original.uri.toString()) { + return this.original; + } else { + return this.modified; + } + } +} + +export class SingleSideDiffElementViewModel extends DiffElementViewModelBase { + get cellViewModel() { + return this.type === 'insert' ? this.modified! : this.original!; + } + + get originalDocument() { + if (this.type === 'insert') { + return this.otherDocumentTextModel; + } else { + return this.mainDocumentTextModel; + } + } + + get modifiedDocument() { + if (this.type === 'insert') { + return this.mainDocumentTextModel; + } else { + return this.otherDocumentTextModel; + } + } + + constructor( + readonly mainDocumentTextModel: NotebookTextModel, + readonly otherDocumentTextModel: NotebookTextModel, + readonly original: DiffNestedCellViewModel | undefined, + readonly modified: DiffNestedCellViewModel | undefined, + readonly type: 'insert' | 'delete', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(mainDocumentTextModel, original, modified, type, editorEventDispatcher); + this._register(this.cellViewModel!.onDidChangeOutputLayout(() => { + this._layout({ recomputeOutput: true }); + })); + } + + getNestedCellViewModel(diffSide: DiffSide): DiffNestedCellViewModel { + return this.type === 'insert' ? this.modified! : this.original!; + } + + + checkIfOutputsModified(): boolean { + return false; + } + + checkMetadataIfModified(): boolean { + return false; + } + + updateOutputHeight(diffSide: DiffSide, index: number, height: number) { + this.cellViewModel?.updateOutputHeight(index, height); + } + + getOutputOffsetInContainer(diffSide: DiffSide, index: number) { + return this.cellViewModel!.getOutputOffset(index); + } + + getOutputOffsetInCell(diffSide: DiffSide, index: number) { + const offsetInOutputsContainer = this.cellViewModel!.getOutputOffset(index); + + return this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin / 2 + + offsetInOutputsContainer; + } + + isOutputEmpty() { + if (this.mainDocumentTextModel.transientOptions.transientOutputs) { + return true; + } + + // outputs are not changed + + return (this.original?.outputs || this.modified?.outputs || []).length === 0; + } + + getRichOutputTotalHeight() { + return this.cellViewModel?.getOutputTotalHeight() ?? 0; + } + + getCellByUri(cellUri: URI): IGenericCellViewModel { + return this.cellViewModel!; + } +} + +export function getFormatedMetadataJSON(documentTextModel: NotebookTextModel, metadata: NotebookCellMetadata, language?: string) { + let filteredMetadata: { [key: string]: any } = {}; + + if (documentTextModel) { + const transientMetadata = documentTextModel.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; + } + } + } else { + filteredMetadata = metadata; + } + + const content = JSON.stringify({ + language, + ...filteredMetadata + }); + + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return metadataSource; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts new file mode 100644 index 0000000000..d67059869a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { IDiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { CellViewModelStateChangeEvent, ICellOutputViewModel, IGenericCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/cellOutputViewModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; + +export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCellViewModel, IGenericCellViewModel { + private _id: string; + get id() { + return this._id; + } + + get outputs() { + return this.textModel.outputs; + } + + get language() { + return this.textModel.language; + } + + get metadata() { + return this.textModel.metadata; + } + + get uri() { + return this.textModel.uri; + } + + get handle() { + return this.textModel.handle; + } + + protected readonly _onDidChangeState: Emitter = this._register(new Emitter()); + + private _hoveringOutput: boolean = false; + public get outputIsHovered(): boolean { + return this._hoveringOutput; + } + + public set outputIsHovered(v: boolean) { + this._hoveringOutput = v; + this._onDidChangeState.fire({ outputIsHoveredChanged: true }); + } + private _outputViewModels: ICellOutputViewModel[]; + + get outputsViewModels() { + return this._outputViewModels; + } + + protected _outputCollection: number[] = []; + protected _outputsTop: PrefixSumComputer | null = null; + protected readonly _onDidChangeOutputLayout = new Emitter(); + readonly onDidChangeOutputLayout = this._onDidChangeOutputLayout.event; + + + constructor( + readonly textModel: NotebookCellTextModel, + @INotebookService private _notebookService: INotebookService + ) { + super(); + this._id = generateUuid(); + + this._outputViewModels = this.textModel.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); + this._register(this.textModel.onDidChangeOutputs((splices) => { + splices.reverse().forEach(splice => { + this._outputCollection.splice(splice[0], splice[1], ...splice[2].map(() => 0)); + this._outputViewModels.splice(splice[0], splice[1], ...splice[2].map(output => new CellOutputViewModel(this, output, this._notebookService))); + }); + + this._outputsTop = null; + this._onDidChangeOutputLayout.fire(); + })); + this._outputCollection = new Array(this.textModel.outputs.length); + } + + private _ensureOutputsTop() { + if (!this._outputsTop) { + const values = new Uint32Array(this._outputCollection.length); + for (let i = 0; i < this._outputCollection.length; i++) { + values[i] = this._outputCollection[i]; + } + + this._outputsTop = new PrefixSumComputer(values); + } + } + + getOutputOffset(index: number): number { + this._ensureOutputsTop(); + + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + return this._outputsTop!.getAccumulatedValue(index - 1); + } + + updateOutputHeight(index: number, height: number): void { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._ensureOutputsTop(); + this._outputCollection[index] = height; + if (this._outputsTop!.changeValue(index, height)) { + this._onDidChangeOutputLayout.fire(); + } + } + + getOutputTotalHeight() { + this._ensureOutputsTop(); + + return this._outputsTop?.getTotalValue() ?? 0; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts new file mode 100644 index 0000000000..1b81ba1fdc --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; +import { NotebookLayoutChangeEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export enum NotebookDiffViewEventType { + LayoutChanged = 1, + CellLayoutChanged = 2 + // MetadataChanged = 2, + // CellStateChanged = 3 +} + +export class NotebookDiffLayoutChangedEvent { + public readonly type = NotebookDiffViewEventType.LayoutChanged; + + constructor(readonly source: NotebookLayoutChangeEvent, readonly value: NotebookLayoutInfo) { + + } +} + +export class NotebookCellLayoutChangedEvent { + public readonly type = NotebookDiffViewEventType.CellLayoutChanged; + + constructor(readonly source: IDiffElementLayoutInfo) { + + } +} + +export type NotebookDiffViewEvent = NotebookDiffLayoutChangedEvent | NotebookCellLayoutChangedEvent; + +export class NotebookDiffEditorEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + protected readonly _onDidChangeCellLayout = new Emitter(); + readonly onDidChangeCellLayout = this._onDidChangeCellLayout.event; + + constructor() { + } + + emit(events: NotebookDiffViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + const e = events[i]; + + switch (e.type) { + case NotebookDiffViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + case NotebookDiffViewEventType.CellLayoutChanged: + this._onDidChangeCellLayout.fire(e); + break; + } + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css index 84504efa9a..2435d9885b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -21,6 +21,31 @@ flex-direction: row; } +.notebook-text-diff-editor .webview-cover { + user-select: initial; + -webkit-user-select: initial; +} + +.notebook-text-diff-editor .cell-body .border-container { + position: absolute; + width: calc(100% - 32px); +} + +.notebook-text-diff-editor .cell-body .border-container .top-border, +.notebook-text-diff-editor .cell-body .border-container .bottom-border { + position: absolute; + width: 100%; +} + +.notebook-text-diff-editor .cell-body .border-container .left-border, +.notebook-text-diff-editor .cell-body .border-container .right-border { + position: absolute; +} + +.notebook-text-diff-editor .cell-body .border-container .right-border { + left: 100%; +} + .notebook-text-diff-editor .cell-body.right { flex-direction: row-reverse; } @@ -32,18 +57,20 @@ .notebook-text-diff-editor .cell-body .cell-diff-editor-container { width: 100%; - overflow: hidden; + /* why we overflow hidden at the beginning?*/ + /* overflow: hidden; */ } .notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff, .notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff, .notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff { /** 100% + diffOverviewWidth */ - width: calc(100% + 30px); + width: calc(100%); } .notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview, -.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview { +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff .monaco-diff-editor .diffOverview { display: none; } @@ -106,10 +133,15 @@ overflow: hidden; } +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { + overflow: visible !important; +} + .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, .monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; + background-color: transparent !important; } .notebook-text-diff-editor .cell-diff-editor-container .editor-input-toolbar-container { @@ -118,3 +150,120 @@ top: 16px; margin: 4px 2px; } + +.monaco-workbench .notebook-text-diff-editor .cell-body { + height: 0; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container { + user-select: text; + -webkit-user-select: text; + -ms-user-select: text; + white-space: initial; + cursor: auto; + position: relative; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container .output-plaintext { + white-space: pre; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container, +.monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { + width: 100%; + padding: 0px 8px; + box-sizing: border-box; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container { + padding: 0px 8px 0px 32px; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { + padding: 0px 8px 0px 32px; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-view-container .output-inner-container { + width: 100%; + padding: 4px 8px 4px 32px; + box-sizing: border-box; + overflow-x: hidden; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left { + top: 0; + position: absolute; + left: 0; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right { + position: absolute; + top: 0; + left: 50%; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left, +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right { + width: 50%; + display: inline-block; +} + +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left div.foreground, +.monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right div.foreground { + width: 100%; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container > div.foreground { + width: 100%; + min-height: 24px; + box-sizing: border-box; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error_message { + color: red; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error > div { + white-space: normal; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error pre.traceback { + box-sizing: border-box; + padding: 8px 0; + margin: 0px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .error .traceback > span { + display: block; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .display img { + max-width: 100%; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .multi-mimetype-output { + position: absolute; + top: 4px; + left: 8px; + width: 16px; + height: 16px; + cursor: pointer; + padding: 2px 4px 4px 2px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-empty-view span { + opacity: 0.7; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-empty-view { + font-style: italic; + height: 24px; + margin: auto; + padding-left: 12px; +} + +.monaco-workbench .notebook-text-diff-editor .output-view-container pre { + margin: 4px 0; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index 6490168cab..7cddd9e6d3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -8,10 +8,11 @@ import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ActiveEditorContext, viewColumnToEditorGroup } from 'vs/workbench/common/editor'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; -import { openAsTextIcon, revertIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { openAsTextIcon, renderOutputIcon, revertIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -60,12 +61,14 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellMetadataTitle - } + id: MenuId.NotebookDiffCellMetadataTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -77,7 +80,55 @@ registerAction2(class extends Action2 { return; } - modified.metadata = original.metadata; + modified.textModel.metadata = original.metadata; + } +}); + +// registerAction2(class extends Action2 { +// constructor() { +// super( +// { +// id: 'notebook.diff.cell.switchOutputRenderingStyle', +// title: localize('notebook.diff.cell.switchOutputRenderingStyle', "Switch Outputs Rendering"), +// icon: renderOutputIcon, +// f1: false, +// menu: { +// id: MenuId.NotebookDiffCellOutputsTitle +// } +// } +// ); +// } +// run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { +// if (!context) { +// return; +// } + +// context.cell.renderOutput = true; +// } +// }); + + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.switchOutputRenderingStyleToText', + title: localize('notebook.diff.cell.switchOutputRenderingStyleToText', "Switch Output Rendering"), + icon: renderOutputIcon, + f1: false, + menu: { + id: MenuId.NotebookDiffCellOutputsTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { + if (!context) { + return; + } + + context.cell.renderOutput = !context.cell.renderOutput; } }); @@ -90,12 +141,14 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellOutputsTitle - } + id: MenuId.NotebookDiffCellOutputsTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return; } @@ -107,10 +160,11 @@ registerAction2(class extends Action2 { return; } - modified.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); + modified.textModel.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); } }); + registerAction2(class extends Action2 { constructor() { super( @@ -120,12 +174,15 @@ registerAction2(class extends Action2 { icon: revertIcon, f1: false, menu: { - id: MenuId.NotebookDiffCellInputTitle - } + id: MenuId.NotebookDiffCellInputTitle, + when: NOTEBOOK_DIFF_CELL_PROPERTY + }, + precondition: NOTEBOOK_DIFF_CELL_PROPERTY + } ); } - run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { if (!context) { return undefined; } @@ -139,7 +196,7 @@ registerAction2(class extends Action2 { const bulkEditService = accessor.get(IBulkEditService); return bulkEditService.apply([ - new ResourceTextEdit(modified.uri, { range: modified.getFullModelRange(), text: original.getValue() }), + new ResourceTextEdit(modified.uri, { range: modified.textModel.getFullModelRange(), text: original.textModel.getValue() }), ], { quotableLabel: 'Split Notebook Cell' }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts new file mode 100644 index 0000000000..131fd439f4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffElementViewModelBase } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { Event } from 'vs/base/common/event'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export enum DiffSide { + Original = 0, + Modified = 1 +} + +export interface IDiffCellInfo extends ICommonCellInfo { + diffElement: DiffElementViewModelBase; +} + +export interface INotebookTextDiffEditor extends ICommonNotebookEditor { + readonly textModel?: NotebookTextModel; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>; + onDidDynamicOutputRendered: Event<{ cell: IGenericCellViewModel, output: IDisplayOutputViewModel }>; + getOverflowContainerDomNode(): HTMLElement; + getLayoutInfo(): NotebookLayoutInfo; + layoutNotebookCell(cell: DiffElementViewModelBase, height: number): void; + getOutputRenderer(): OutputRenderer; + createInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void; + showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide): void; + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IDisplayOutputViewModel, diffSide: DiffSide): void; + hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: IDiffNestedCellViewModel, output: IDisplayOutputViewModel): void; + /** + * Trigger the editor to scroll from scroll event programmatically + */ + triggerScroll(event: IMouseWheelEvent): void; + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; + deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]): void; +} + +export interface IDiffNestedCellViewModel { + +} + +export interface CellDiffCommonRenderTemplate { + readonly leftBorder: HTMLElement; + readonly rightBorder: HTMLElement; + readonly topBorder: HTMLElement; + readonly bottomBorder: HTMLElement; +} + +export interface CellDiffSingleSideRenderTemplate extends CellDiffCommonRenderTemplate { + readonly container: HTMLElement; + readonly body: HTMLElement; + readonly diffEditorContainer: HTMLElement; + readonly diagonalFill: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly sourceEditor: CodeEditorWidget; + readonly metadataHeaderContainer: HTMLElement; + readonly metadataInfoContainer: HTMLElement; + readonly outputHeaderContainer: HTMLElement; + readonly outputInfoContainer: HTMLElement; + +} + + +export interface CellDiffSideBySideRenderTemplate extends CellDiffCommonRenderTemplate { + readonly container: HTMLElement; + readonly body: HTMLElement; + readonly diffEditorContainer: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly sourceEditor: DiffEditorWidget; + readonly editorContainer: HTMLElement; + readonly inputToolbarContainer: HTMLElement; + readonly toolbar: ToolBar; + readonly metadataHeaderContainer: HTMLElement; + readonly metadataInfoContainer: HTMLElement; + readonly outputHeaderContainer: HTMLElement; + readonly outputInfoContainer: HTMLElement; +} + +export interface IDiffElementLayoutInfo { + totalHeight: number; + width: number; + editorHeight: number; + editorMargin: number; + metadataHeight: number; + metadataStatusHeight: number; + rawOutputHeight: number; + outputTotalHeight: number; + outputStatusHeight: number; + bodyMargin: number +} + +type IDiffElementSelfLayoutChangeEvent = { [K in keyof IDiffElementLayoutInfo]?: boolean }; + +export interface CellDiffViewModelLayoutChangeEvent extends IDiffElementSelfLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: boolean; + metadataEditor?: boolean; + outputEditor?: boolean; + outputView?: boolean; +} + +export const DIFF_CELL_MARGIN = 16; +export const NOTEBOOK_DIFF_CELL_PROPERTY = new RawContextKey('notebookDiffCellPropertyChanged', false); +export const NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED = new RawContextKey('notebookDiffCellPropertyExpanded', false); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 8409852c2a..32ed2630fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -13,32 +13,38 @@ import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/n import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CellDiffSideBySideRenderer, CellDiffSingleSideRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { getZoomLevel } from 'vs/base/browser/browser'; -import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; +import { IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffSide, DIFF_CELL_MARGIN, IDiffCellInfo, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IDiffChange } from 'vs/base/common/diff/diff'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { SequencerByKey } from 'vs/base/common/async'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; -export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); +const $ = DOM.$; export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor { static readonly ID: string = 'workbench.editor.notebookTextDiffEditor'; @@ -46,21 +52,38 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _rootElement!: HTMLElement; private _overflowContainer!: HTMLElement; private _dimension: DOM.Dimension | null = null; - private _list!: WorkbenchList; + private _diffElementViewModels: DiffElementViewModelBase[] = []; + private _list!: NotebookTextDiffList; + private _modifiedWebview: BackLayerWebView | null = null; + private _originalWebview: BackLayerWebView | null = null; + private _webviewTransparentCover: HTMLElement | null = null; private _fontInfo: BareFontInfo | undefined; - private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>()); + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: DiffElementViewModelBase; }>()); public readonly onMouseUp = this._onMouseUp.event; private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; protected _scopeContextKeyService!: IContextKeyService; private _model: INotebookDiffEditorModel | null = null; private _modifiedResourceDisposableStore = new DisposableStore(); + private _outputRenderer: OutputRenderer; get textModel() { return this._model?.modified.notebook; } private _revealFirst: boolean; + private readonly _insetModifyQueueByOutputId = new SequencerByKey(); + + protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: IDisplayOutputViewModel }>(); + onDidDynamicOutputRendered = this._onDidDynamicOutputRendered.event; + + private _localStore: DisposableStore = this._register(new DisposableStore()); + + private _isDisposed: boolean = false; + + get isDisposed() { + return this._isDisposed; + } constructor( @IInstantiationService readonly instantiationService: IInstantiationService, @@ -75,10 +98,40 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD ) { super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); const editorOptions = this.configurationService.getValue('editor'); - this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); this._revealFirst = true; this._register(this._modifiedResourceDisposableStore); + this._outputRenderer = new OutputRenderer(this, this.instantiationService); + } + + focusNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + // throw new Error('Method not implemented.'); + } + + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + // throw new Error('Method not implemented.'); + } + + updateOutputHeight(cellInfo: IDiffCellInfo, output: IDisplayOutputViewModel, outputHeight: number, isInit: boolean): void { + const diffElement = cellInfo.diffElement; + const cell = this.getCellByInfo(cellInfo); + const outputIndex = cell.outputsViewModels.indexOf(output); + + if (diffElement instanceof SideBySideDiffElementViewModel) { + const info = CellUri.parse(cellInfo.cellUri); + if (!info) { + return; + } + + diffElement.updateOutputHeight(info.notebook.toString() === this._model?.original.resource.toString() ? DiffSide.Original : DiffSide.Modified, outputIndex, outputHeight); + } else { + diffElement.updateOutputHeight(diffElement.type === 'insert' ? DiffSide.Modified : DiffSide.Original, outputIndex, outputHeight); + } + + if (isInit) { + this._onDidDynamicOutputRendered.fire({ cell, output }); + } } protected createEditor(parent: HTMLElement): void { @@ -87,16 +140,17 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); - const renderer = this.instantiationService.createInstance(CellDiffRenderer, this); + const renderers = [ + this.instantiationService.createInstance(CellDiffSingleSideRenderer, this), + this.instantiationService.createInstance(CellDiffSideBySideRenderer, this), + ]; this._list = this.instantiationService.createInstance( NotebookTextDiffList, 'NotebookTextDiff', this._rootElement, this.instantiationService.createInstance(NotebookCellTextDiffListDelegate), - [ - renderer - ], + renderers, this.contextKeyService, { setRowLineHeight: false, @@ -140,17 +194,94 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } ); + this._register(this._list); + this._register(this._list.onMouseUp(e => { if (e.element) { this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); } })); + + // transparent cover + this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); + this._webviewTransparentCover.style.display = 'none'; + + this._register(DOM.addStandardDisposableGenericMouseDownListner(this._overflowContainer, (e: StandardMouseEvent) => { + if (e.target.classList.contains('slider') && this._webviewTransparentCover) { + this._webviewTransparentCover.style.display = 'block'; + } + })); + + this._register(DOM.addStandardDisposableGenericMouseUpListner(this._overflowContainer, () => { + if (this._webviewTransparentCover) { + // no matter when + this._webviewTransparentCover.style.display = 'none'; + } + })); + + this._register(this._list.onDidScroll(e => { + this._webviewTransparentCover!.style.top = `${e.scrollTop}px`; + })); + + + } + + private _updateOutputsOffsetsInWebview(scrollTop: number, scrollHeight: number, activeWebview: BackLayerWebView, getActiveNestedCell: (diffElement: DiffElementViewModelBase) => DiffNestedCellViewModel | undefined, diffSide: DiffSide) { + activeWebview.element.style.height = `${scrollHeight}px`; + + if (activeWebview.insetMapping) { + const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; + const removedItems: IDisplayOutputViewModel[] = []; + activeWebview.insetMapping.forEach((value, key) => { + const cell = getActiveNestedCell(value.cellInfo.diffElement); + if (!cell) { + return; + } + + const viewIndex = this._list.indexOf(value.cellInfo.diffElement); + + if (viewIndex === undefined) { + return; + } + + if (cell.outputsViewModels.indexOf(key) < 0) { + // output is already gone + removedItems.push(key); + } else { + const cellTop = this._list.getAbsoluteTopOfElement(value.cellInfo.diffElement); + if (activeWebview.shouldUpdateInset(cell, key, cellTop)) { + const outputIndex = cell.outputsViewModels.indexOf(key); + const outputOffset = cellTop + value.cellInfo.diffElement.getOutputOffsetInCell(diffSide, outputIndex); + + updateItems.push({ + output: key, + cellTop: cellTop, + outputOffset: outputOffset + }); + } + } + + }); + + removedItems.forEach(output => activeWebview.removeInset(output)); + + if (updateItems.length) { + activeWebview.updateViewScrollTop(-scrollTop, false, updateItems); + } + } } async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); - this._model = await input.resolve(); + const model = await input.resolve(); + if (this._model !== model) { + this._detachModel(); + this._model = model; + this._attachModel(); + } + + this._model = model; if (this._model === null) { return; } @@ -196,11 +327,73 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } })); - - this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + await this._createOriginalWebview(generateUuid(), this._model.original.resource); + await this._createModifiedWebview(generateUuid(), this._model.modified.resource); await this.updateLayout(); } + private _detachModel() { + this._localStore.clear(); + this._originalWebview?.dispose(); + this._originalWebview?.element.remove(); + this._originalWebview = null; + this._modifiedWebview?.dispose(); + this._modifiedWebview?.element.remove(); + this._modifiedWebview = null; + + this._modifiedResourceDisposableStore.clear(); + this._list.clear(); + + } + private _attachModel() { + this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + const updateInsets = () => { + DOM.scheduleAtNextAnimationFrame(() => { + if (this._isDisposed) { + return; + } + + if (this._modifiedWebview) { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._modifiedWebview, (diffElement: DiffElementViewModelBase) => { + return diffElement.modified; + }, DiffSide.Modified); + } + + if (this._originalWebview) { + this._updateOutputsOffsetsInWebview(this._list.scrollTop, this._list.scrollHeight, this._originalWebview, (diffElement: DiffElementViewModelBase) => { + return diffElement.original; + }, DiffSide.Original); + } + }); + }; + + this._localStore.add(this._list.onDidChangeContentHeight(() => { + updateInsets(); + })); + + this._localStore.add(this._eventDispatcher.onDidChangeCellLayout(() => { + updateInsets(); + })); + } + + private async _createModifiedWebview(id: string, resource: URI): Promise { + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: 32 }) as BackLayerWebView; + // attach the webview container to the DOM tree first + this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); + await this._modifiedWebview.createWebview(); + this._modifiedWebview.element.style.width = `calc(50% - 16px)`; + this._modifiedWebview.element.style.left = `calc(50%)`; + } + + private async _createOriginalWebview(id: string, resource: URI): Promise { + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: 32 }) as BackLayerWebView; + // attach the webview container to the DOM tree first + this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); + await this._originalWebview.createWebview(); + this._originalWebview.element.style.width = `calc(50% - 16px)`; + this._originalWebview.element.style.left = `16px`; + } + private async _resolveStats(resource: URI) { if (resource.scheme === Schemas.untitled) { return undefined; @@ -222,7 +415,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource); const cellChanges = diffResult.cellsDiff.changes; - const cellDiffViewModels: CellDiffViewModel[] = []; + const diffElementViewModels: DiffElementViewModelBase[] = []; const originalModel = this._model.original.notebook; const modifiedModel = this._model.modified.notebook; let originalCellIndex = 0; @@ -238,20 +431,24 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const originalCell = originalModel.cells[originalCellIndex + j]; const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; if (originalCell.getHashValue() === modifiedCell.getHashValue()) { - cellDiffViewModels.push(new CellDiffViewModel( - originalCell, - modifiedCell, + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalCell), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedCell), 'unchanged', this._eventDispatcher! )); } else { if (firstChangeIndex === -1) { - firstChangeIndex = cellDiffViewModels.length; + firstChangeIndex = diffElementViewModels.length; } - cellDiffViewModels.push(new CellDiffViewModel( - originalCell, - modifiedCell, + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalCell), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedCell), 'modified', this._eventDispatcher! )); @@ -260,24 +457,35 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const modifiedLCS = this._computeModifiedLCS(change, originalModel, modifiedModel); if (modifiedLCS.length && firstChangeIndex === -1) { - firstChangeIndex = cellDiffViewModels.length; + firstChangeIndex = diffElementViewModels.length; } - cellDiffViewModels.push(...modifiedLCS); + diffElementViewModels.push(...modifiedLCS); originalCellIndex = change.originalStart + change.originalLength; modifiedCellIndex = change.modifiedStart + change.modifiedLength; } for (let i = originalCellIndex; i < originalModel.cells.length; i++) { - cellDiffViewModels.push(new CellDiffViewModel( - originalModel.cells[i], - modifiedModel.cells[i - originalCellIndex + modifiedCellIndex], + diffElementViewModels.push(new SideBySideDiffElementViewModel( + this._model.modified.notebook, + this._model.original.notebook, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[i]), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[i - originalCellIndex + modifiedCellIndex]), 'unchanged', this._eventDispatcher! )); } - this._list.splice(0, this._list.length, cellDiffViewModels); + this._originalWebview?.insetMapping.forEach((value, key) => { + this._originalWebview?.removeInset(key); + }); + + this._modifiedWebview?.insetMapping.forEach((value, key) => { + this._modifiedWebview?.removeInset(key); + }); + + this._diffElementViewModels = diffElementViewModels; + this._list.splice(0, this._list.length, diffElementViewModels); if (this._revealFirst && firstChangeIndex !== -1) { this._revealFirst = false; @@ -287,14 +495,16 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } private _computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { - const result: CellDiffViewModel[] = []; + const result: DiffElementViewModelBase[] = []; // modified cells const modifiedLen = Math.min(change.originalLength, change.modifiedLength); for (let j = 0; j < modifiedLen; j++) { - result.push(new CellDiffViewModel( - originalModel.cells[change.originalStart + j], - modifiedModel.cells[change.modifiedStart + j], + result.push(new SideBySideDiffElementViewModel( + modifiedModel, + originalModel, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[change.originalStart + j]), + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[change.modifiedStart + j]), 'modified', this._eventDispatcher! )); @@ -302,8 +512,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD for (let j = modifiedLen; j < change.originalLength; j++) { // deletion - result.push(new CellDiffViewModel( - originalModel.cells[change.originalStart + j], + result.push(new SingleSideDiffElementViewModel( + originalModel, + modifiedModel, + this.instantiationService.createInstance(DiffNestedCellViewModel, originalModel.cells[change.originalStart + j]), undefined, 'delete', this._eventDispatcher! @@ -312,9 +524,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD for (let j = modifiedLen; j < change.modifiedLength; j++) { // insertion - result.push(new CellDiffViewModel( + result.push(new SingleSideDiffElementViewModel( + modifiedModel, + originalModel, undefined, - modifiedModel.cells[change.modifiedStart + j], + this.instantiationService.createInstance(DiffNestedCellViewModel, modifiedModel.cells[change.modifiedStart + j]), 'insert', this._eventDispatcher! )); @@ -323,14 +537,12 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return result; } - private pendingLayouts = new WeakMap(); + private pendingLayouts = new WeakMap(); - layoutNotebookCell(cell: CellDiffViewModel, height: number) { - const relayout = (cell: CellDiffViewModel, height: number) => { - const viewIndex = this._list.indexOf(cell); - - this._list?.updateElementHeight(viewIndex, height); + layoutNotebookCell(cell: DiffElementViewModelBase, height: number) { + const relayout = (cell: DiffElementViewModelBase, height: number) => { + this._list.updateElementHeight2(cell, height); }; if (this.pendingLayouts.has(cell)) { @@ -353,6 +565,79 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return new Promise(resolve => { r = resolve; }); } + triggerScroll(event: IMouseWheelEvent) { + this._list.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IInsetRenderOutput, getOffset: () => number, diffSide: DiffSide): void { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(output.source)) { + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + await activeWebview.createInset({ diffElement: cellDiffViewModel, cellHandle: cellViewModel.handle, cellId: cellViewModel.id, cellUri: cellViewModel.uri }, output, cellTop, getOffset()); + } else { + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const scrollTop = this._list.scrollTop; + const outputIndex = cellViewModel.outputsViewModels.indexOf(output.source); + const outputOffset = cellTop + cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); + activeWebview.updateViewScrollTop(-scrollTop, true, [{ output: output.source, cellTop, outputOffset }]); + } + }); + } + + getCellByInfo(cellInfo: IDiffCellInfo): IGenericCellViewModel { + return cellInfo.diffElement.getCellByUri(cellInfo.cellUri); + } + + removeInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide) { + this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(displayOutput)) { + return; + } + + activeWebview.removeInset(displayOutput); + }); + } + + showInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, displayOutput: IDisplayOutputViewModel, diffSide: DiffSide) { + this._insetModifyQueueByOutputId.queue(displayOutput.model.outputId + (diffSide === DiffSide.Modified ? '-right' : 'left'), async () => { + const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + if (!activeWebview) { + return; + } + + if (!activeWebview.insetMapping.has(displayOutput)) { + return; + } + + const cellTop = this._list.getAbsoluteTopOfElement(cellDiffViewModel); + const scrollTop = this._list.scrollTop; + const outputIndex = cellViewModel.outputsViewModels.indexOf(displayOutput); + const outputOffset = cellTop + cellDiffViewModel.getOutputOffsetInCell(diffSide, outputIndex); + activeWebview.updateViewScrollTop(-scrollTop, true, [{ output: displayOutput, cellTop, outputOffset }]); + }); + } + + hideInset(cellDiffViewModel: DiffElementViewModelBase, cellViewModel: DiffNestedCellViewModel, output: IDisplayOutputViewModel) { + this._modifiedWebview?.hideInset(output); + this._originalWebview?.hideInset(output); + } + + // private async _resolveWebview(rightEditor: boolean): Promise { + // if (rightEditor) { + + // } + // } + getDomNode() { return this._rootElement; } @@ -380,6 +665,18 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._list?.splice(0, this._list?.length || 0); } + getOutputRenderer(): OutputRenderer { + return this._outputRenderer; + } + + deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]) { + if (diffSide === DiffSide.Original) { + this._originalWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } else { + this._modifiedWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } + } + getLayoutInfo(): NotebookLayoutInfo { if (!this._list) { throw new Error('Editor is not initalized successfully'); @@ -392,6 +689,56 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }; } + getCellOutputLayoutInfo(nestedCell: DiffNestedCellViewModel) { + if (!this._model) { + throw new Error('Editor is not attached to model yet'); + } + const documentModel = CellUri.parse(nestedCell.uri); + if (!documentModel) { + throw new Error('Nested cell in the diff editor has wrong Uri'); + } + + const belongToOriginalDocument = this._model.original.notebook.uri.toString() === documentModel.notebook.toString(); + const viewModel = this._diffElementViewModels.find(element => { + const textModel = belongToOriginalDocument ? element.original : element.modified; + if (!textModel) { + return false; + } + + if (textModel.uri.toString() === nestedCell.uri.toString()) { + return true; + } + + return false; + }); + + if (!viewModel) { + throw new Error('Nested cell in the diff editor does not match any diff element'); + } + + if (viewModel.type === 'unchanged') { + return this.getLayoutInfo(); + } + + if (viewModel.type === 'insert' || viewModel.type === 'delete') { + return { + width: this._dimension!.width / 2, + height: this._dimension!.height / 2, + fontInfo: this._fontInfo! + }; + } + + if (viewModel.checkIfOutputsModified()) { + return { + width: this._dimension!.width / 2, + height: this._dimension!.height / 2, + fontInfo: this._fontInfo! + }; + } else { + return this.getLayoutInfo(); + } + } + layout(dimension: DOM.Dimension): void { this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); this._rootElement.classList.toggle('narrow-width', dimension.width < 600); @@ -399,14 +746,39 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._rootElement.style.height = `${dimension.height}px`; this._list?.layout(this._dimension.height, this._dimension.width); - this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + + + if (this._modifiedWebview) { + this._modifiedWebview.element.style.width = `calc(50% - 16px)`; + this._modifiedWebview.element.style.left = `calc(50%)`; + } + + if (this._originalWebview) { + this._originalWebview.element.style.width = `calc(50% - 16px)`; + this._originalWebview.element.style.left = `16px`; + } + + if (this._webviewTransparentCover) { + this._webviewTransparentCover.style.height = `${dimension.height}px`; + this._webviewTransparentCover.style.width = `${dimension.width}px`; + } + + this._eventDispatcher?.emit([new NotebookDiffLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } + + dispose() { + this._isDisposed = true; + super.dispose(); } } registerThemingParticipant((theme, collector) => { const cellBorderColor = theme.getColor(notebookCellBorder); if (cellBorderColor) { - collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .top-border { border-top: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .bottom-border { border-top: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .left-border { border-left: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-body .border-container .right-border { border-right: 1px solid ${cellBorderColor};}`); collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { border-top: 1px solid ${cellBorderColor}; @@ -429,6 +801,13 @@ registerThemingParticipant((theme, collector) => { const added = theme.getColor(diffInserted); if (added) { + collector.addRule( + ` + .monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-right div.foreground { background-color: ${added}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-info-container .output-view-container div.foreground { background-color: ${added}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-info-container .output-view-container div.output-empty-view { background-color: ${added}; } + ` + ); collector.addRule(` .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; } .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, @@ -460,7 +839,15 @@ registerThemingParticipant((theme, collector) => { ); } const removed = theme.getColor(diffRemoved); - if (added) { + if (removed) { + collector.addRule( + ` + .monaco-workbench .notebook-text-diff-editor .cell-body.full .output-info-container.modified .output-view-container .output-view-container-left div.foreground { background-color: ${removed}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-info-container .output-view-container div.foreground { background-color: ${removed}; } + .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-info-container .output-view-container div.output-empty-view { background-color: ${removed}; } + + ` + ); collector.addRule(` .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; } .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index 984b6f3a8e..dffa04d756 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -14,12 +14,20 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; -import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { DiffElementViewModelBase, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { isMacintosh } from 'vs/base/common/platform'; -import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents'; +import { DeletedElement, fixedDiffEditorOptions, fixedEditorOptions, getOptimizedNestedCodeEditorWidgetOptions, InsertElement, ModifiedElement } from 'vs/workbench/contrib/notebook/browser/diff/diffComponents'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { // private readonly lineHeight: number; constructor( @@ -29,20 +37,28 @@ export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { - static readonly TEMPLATE_ID = 'cell_diff'; +export class CellDiffSingleSideRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff_single'; constructor( readonly notebookEditor: INotebookTextDiffEditor, @@ -50,56 +66,228 @@ export class CellDiffRenderer implements IListRenderer implements IDisposable, IStyleController { +export class CellDiffSideBySideRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff_side_by_side'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IContextMenuService protected readonly contextMenuService: IContextMenuService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @IMenuService protected readonly menuService: IMenuService, + @IContextKeyService protected readonly contextKeyService: IContextKeyService, + @INotificationService protected readonly notificationService: INotificationService, + ) { } + + get templateId() { + return CellDiffSideBySideRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffSideBySideRenderTemplate { + const body = DOM.$('.cell-body'); + DOM.append(container, body); + const diffEditorContainer = DOM.$('.cell-diff-editor-container'); + DOM.append(body, diffEditorContainer); + + const sourceContainer = DOM.append(diffEditorContainer, DOM.$('.source-container')); + const { editor, editorContainer } = this._buildSourceEditor(sourceContainer); + + const inputToolbarContainer = DOM.append(sourceContainer, DOM.$('.editor-input-toolbar-container')); + const cellToolbarContainer = DOM.append(inputToolbarContainer, DOM.$('div.property-toolbar')); + const toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService); + return item; + } + + return undefined; + } + }); + + const metadataHeaderContainer = DOM.append(diffEditorContainer, DOM.$('.metadata-header-container')); + const metadataInfoContainer = DOM.append(diffEditorContainer, DOM.$('.metadata-info-container')); + + const outputHeaderContainer = DOM.append(diffEditorContainer, DOM.$('.output-header-container')); + const outputInfoContainer = DOM.append(diffEditorContainer, DOM.$('.output-info-container')); + + const borderContainer = DOM.append(body, DOM.$('.border-container')); + const leftBorder = DOM.append(borderContainer, DOM.$('.left-border')); + const rightBorder = DOM.append(borderContainer, DOM.$('.right-border')); + const topBorder = DOM.append(borderContainer, DOM.$('.top-border')); + const bottomBorder = DOM.append(borderContainer, DOM.$('.bottom-border')); + + + return { + body, + container, + diffEditorContainer, + sourceEditor: editor, + editorContainer, + inputToolbarContainer, + toolbar, + metadataHeaderContainer, + metadataInfoContainer, + outputHeaderContainer, + outputInfoContainer, + leftBorder, + rightBorder, + topBorder, + bottomBorder, + elementDisposables: new DisposableStore() + }; + } + + private _buildSourceEditor(sourceContainer: HTMLElement) { + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + const editor = this.instantiationService.createInstance(DiffEditorWidget, editorContainer, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + originalEditable: false, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: 0, + width: 0 + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + + return { + editor, + editorContainer + }; + } + + renderElement(element: SideBySideDiffElementViewModel, index: number, templateData: CellDiffSideBySideRenderTemplate, height: number | undefined): void { + templateData.body.classList.remove('left', 'right', 'full'); + + switch (element.type) { + case 'unchanged': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedElement, this.notebookEditor, element, templateData)); + return; + case 'modified': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedElement, this.notebookEditor, element, templateData)); + return; + default: + break; + } + } + + disposeTemplate(templateData: CellDiffSideBySideRenderTemplate): void { + templateData.container.innerText = ''; + templateData.sourceEditor.dispose(); + templateData.toolbar?.dispose(); + } + + disposeElement(element: SideBySideDiffElementViewModel, index: number, templateData: CellDiffSideBySideRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { private styleElement?: HTMLStyleElement; + get rowsContainer(): HTMLElement { + return this.view.containerDomNode; + } + constructor( listUser: string, container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], + delegate: IListVirtualDelegate, + renderers: IListRenderer[], contextKeyService: IContextKeyService, - options: IWorkbenchListOptions, + options: IWorkbenchListOptions, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @@ -107,6 +295,32 @@ export class NotebookTextDiffList extends WorkbenchList imple super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); } + getAbsoluteTopOfElement(element: DiffElementViewModelBase): number { + const index = this.indexOf(element); + // if (index === undefined || index < 0 || index >= this.length) { + // this._getViewIndexUpperBound(element); + // throw new ListError(this.listUser, `Invalid index ${index}`); + // } + + return this.view.elementTop(index); + } + + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.view.triggerScrollFromMouseWheelEvent(browserEvent); + } + + clear() { + super.splice(0, this.length); + } + + + updateElementHeight2(element: DiffElementViewModelBase, size: number) { + const viewIndex = this.indexOf(element); + const focused = this.getFocus(); + + this.view.updateElementHeight(viewIndex, size, focused.length ? focused[0] : null); + } + style(styles: IListStyles) { const selectorSuffix = this.view.domId; if (!this.styleElement) { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 3f34016a6f..79f276f71c 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -184,11 +184,15 @@ .monaco-workbench .notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; - left: -26px; + left: -30px; width: 16px; height: 16px; cursor: pointer; - padding: 4px; + padding: 6px; +} + +.monaco-workbench .notebookOverlay .output pre { + margin: 4px 0; } .monaco-workbench .notebookOverlay .output .error_message { @@ -248,15 +252,17 @@ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { + position: relative; box-sizing: border-box; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part .codicon { - margin-top: 4px; - position: relative; - left: -23px; + position: absolute; + padding: 2px 6px; + left: -30px; + bottom: 0; cursor: pointer; - z-index: 27; /* Over drag handle */ + z-index: 29; /* Over drag handle and bottom cell toolbar */ } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.collapsed .notebook-folding-indicator, @@ -422,6 +428,10 @@ margin-right: 2px; } +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left > div:first-child { + margin-left: 18px; +} + .monaco-workbench .notebookOverlay .cell-statusbar-container .codicon { font-size: 14px; } @@ -475,6 +485,10 @@ text-align: center; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .output-collapsed .execution-count-label { + bottom: 24px; +} + .monaco-workbench .notebookOverlay .cell .cell-editor-part { position: relative; } @@ -535,6 +549,11 @@ cursor: grab; } +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { + z-index: 25; + cursor: default; +} + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator .codicon:hover { cursor: pointer; } @@ -580,7 +599,7 @@ display: flex; align-items: center; justify-content: center; - z-index: 25; /* over the focus outline on the editor, below the title toolbar */ + z-index: 28; /* over the focus outline on the editor, below the title toolbar */ width: 100%; opacity: 0; transition: opacity 0.2s ease-in-out; @@ -593,8 +612,9 @@ .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-bottom-toolbar-container, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-bottom-toolbar-container, +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within { opacity: 1; } @@ -858,3 +878,18 @@ .cell-contributed-items.cell-contributed-items-left { margin-left: 4px; } + +.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar { + width: 10px; /* width of the entire scrollbar */ + height: 10px; +} + +.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { + height: 10px; + width: 10px; +} + +.monaco-workbench .notebookOverlay .output .output-plaintext { + margin: 4px 0; + overflow-x: auto; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 888cf34991..2298f134bd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -24,19 +24,19 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { EditorInput, Extensions as EditorInputExtensions, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { EditorInput, Extensions as EditorInputExtensions, ICustomEditorInputFactory, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, IN_NOTEBOOK_TEXT_DIFF_EDITOR, NotebookEditorOptions, NOTEBOOK_EDITOR_OPEN } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -47,11 +47,15 @@ import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/comm import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl'; +import { INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { Event } from 'vs/base/common/event'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; // Editor Contribution @@ -59,7 +63,7 @@ import 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; -import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; +import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; // import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm'; @@ -162,30 +166,6 @@ class NotebookEditorFactory implements IEditorInputFactory { const input = NotebookEditorInput.create(instantiationService, resource, name, viewType); return input; } - - static async createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { - return instantiationService.invokeFunction(async accessor => { - const backupFileService = accessor.get(IBackupFileService); - - const backup = await backupFileService.resolve(resource); - if (!backup?.meta) { - throw new Error(`No backup found for Notebook editor: ${resource}`); - } - - const input = NotebookEditorInput.create(instantiationService, resource, backup.meta.name, backup.meta.viewType, { startDirty: true }); - return input; - }); - } - - static canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { - if (editorInput instanceof NotebookEditorInput) { - if (isEqual(editorInput.resource.with({ scheme: Schemas.vscodeNotebook }), backupResource)) { - return true; - } - } - - return false; - } } Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( @@ -195,7 +175,31 @@ Registry.as(EditorInputExtensions.EditorInputFactor Registry.as(EditorInputExtensions.EditorInputFactories).registerCustomEditorInputFactory( Schemas.vscodeNotebook, - NotebookEditorFactory + new class implements ICustomEditorInputFactory { + async createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { + return instantiationService.invokeFunction(async accessor => { + const backupFileService = accessor.get(IBackupFileService); + + const backup = await backupFileService.resolve(resource); + if (!backup?.meta) { + throw new Error(`No backup found for Notebook editor: ${resource}`); + } + + const input = NotebookEditorInput.create(instantiationService, resource, backup.meta.name, backup.meta.viewType, { startDirty: true }); + return input; + }); + } + + canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { + if (editorInput instanceof NotebookEditorInput) { + if (isEqual(editorInput.resource.with({ scheme: Schemas.vscodeNotebook }), backupResource)) { + return true; + } + } + + return false; + } + } ); Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( @@ -204,17 +208,24 @@ Registry.as(EditorInputExtensions.EditorInputFactor ); export class NotebookContribution extends Disposable implements IWorkbenchContribution { + private _notebookEditorIsOpen: IContextKey; + private _notebookDiffEditorIsOpen: IContextKey; constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IEditorService private readonly editorService: IEditorService, @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IUndoRedoService undoRedoService: IUndoRedoService, ) { super(); + this._notebookEditorIsOpen = NOTEBOOK_EDITOR_OPEN.bindTo(this.contextKeyService); + this._notebookDiffEditorIsOpen = IN_NOTEBOOK_TEXT_DIFF_EDITOR.bindTo(this.contextKeyService); + this._register(undoRedoService.registerUriComparisonKeyComputer(CellUri.scheme, { getComparisonKey: (uri: URI): string => { return getCellUndoRedoComparisonKey(uri); @@ -224,12 +235,28 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this._register(this.editorService.overrideOpenEditor({ getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => { - const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource)); + const currentEditorsForResource = group && this.editorService.findEditors(resource, group); + const currentEditorForResource = currentEditorsForResource && currentEditorsForResource.length ? currentEditorsForResource[0] : undefined; const associatedEditors = distinct([ ...this.getUserAssociatedNotebookEditors(resource), ...this.getContributedEditors(resource) - ], editor => editor.id); + ], editor => editor.id).sort((a, b) => { + // if a content provider is exclusive, it has higher order + if (a.exclusive && b.exclusive) { + return 0; + } + + if (a.exclusive) { + return -1; + } + + if (b.exclusive) { + return 1; + } + + return 0; + }); return associatedEditors.map(info => { return { @@ -264,6 +291,19 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri this.notebookService.updateActiveNotebookEditor(null); } })); + + this.editorGroupService.whenRestored.then(() => this._updateContextKeys()); + this._register(this.editorService.onDidActiveEditorChange(() => this._updateContextKeys())); + this._register(this.editorService.onDidVisibleEditorsChange(() => this._updateContextKeys())); + + this._register(this.editorGroupService.onDidAddGroup(() => this._updateContextKeys())); + this._register(this.editorGroupService.onDidRemoveGroup(() => this._updateContextKeys())); + } + + private _updateContextKeys() { + const activeEditorPane = this.editorService.activeEditorPane as { isNotebookEditor?: boolean } | undefined; + this._notebookEditorIsOpen.set(!!activeEditorPane?.isNotebookEditor); + this._notebookDiffEditorIsOpen.set(this.editorService.activeEditorPane?.getId() === 'workbench.editor.notebookTextDiffEditor'); } getUserAssociatedEditors(resource: URI) { @@ -300,8 +340,70 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - if (originalInput instanceof NotebookEditorInput) { - return undefined; + // Run reopen with ... + if (id) { + // from the editor tab context menu + if (originalInput instanceof NotebookEditorInput) { + if (originalInput.viewType === id) { + // reopen with the same type + return undefined; + } else { + return { + override: (async () => { + const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource, originalInput.getName(), id); + const originalEditorIndex = group.getIndexOfEditor(originalInput); + + await group.closeEditor(originalInput); + originalInput.dispose(); + const newEditor = await group.openEditor(notebookInput, { ...options, index: originalEditorIndex, override: false }); + if (newEditor) { + return newEditor; + } else { + return undefined; + } + })() + }; + } + } else { + // from the file explorer + const existingEditors = this.editorService.findEditors(originalInput.resource, group).filter(editor => editor instanceof NotebookEditorInput) as NotebookEditorInput[]; + + if (existingEditors.length) { + // there are notebook editors with the same resource + + if (existingEditors.find(editor => editor.viewType === id)) { + return { override: this.editorService.openEditor(existingEditors.find(editor => editor.viewType === id)!, { ...options, override: false }, group) }; + } else { + return { + override: (async () => { + const firstEditor = existingEditors[0]!; + const originalEditorIndex = group.getIndexOfEditor(firstEditor); + + await group.closeEditor(firstEditor); + firstEditor.dispose(); + const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource!, originalInput.getName(), id); + const newEditor = await group.openEditor(notebookInput, { ...options, index: originalEditorIndex, override: false }); + + if (newEditor) { + return newEditor; + } else { + return undefined; + } + })() + }; + } + } + } + } + + // Click on the editor tab + if (id === undefined && originalInput instanceof NotebookEditorInput) { + const existingEditors = this.editorService.findEditors(originalInput.resource, group).filter(editor => editor instanceof NotebookEditorInput && editor === originalInput); + + if (existingEditors.length) { + // same + return undefined; + } } if (originalInput instanceof NotebookDiffEditorInput) { @@ -323,10 +425,8 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri } if (id === undefined) { - const existingEditors = group.editors.filter(editor => - editor.resource - && isEqual(editor.resource, notebookUri) - && !(editor instanceof NotebookEditorInput) + const existingEditors = this.editorService.findEditors(notebookUri, group).filter(editor => + !(editor instanceof NotebookEditorInput) && !(editor instanceof NotebookDiffEditorInput) ); @@ -391,7 +491,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput)); + const existingEditors = this.editorService.findEditors(notebookUri, group).filter(editor => !(editor instanceof NotebookEditorInput)); if (existingEditors.length) { return undefined; @@ -462,7 +562,7 @@ class CellContentProvider implements ITextModelContentProvider { create: (defaultEOL) => { const newEOL = (defaultEOL === DefaultEndOfLine.CRLF ? '\r\n' : '\n'); (cell.textBuffer as ITextBuffer).setEOL(newEOL); - return cell.textBuffer as ITextBuffer; + return { textBuffer: cell.textBuffer as ITextBuffer, disposable: Disposable.None }; }, getFirstLineText: (limit: number) => { return cell.textBuffer.getLineContent(1).substr(0, limit); @@ -489,6 +589,61 @@ class CellContentProvider implements ITextModelContentProvider { } } +class CellMetadataContentProvider implements ITextModelContentProvider { + private readonly _registration: IDisposable; + + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly _modelService: IModelService, + @IModeService private readonly _modeService: IModeService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + ) { + this._registration = textModelService.registerTextModelContentProvider(Schemas.vscodeNotebookCellMetadata, this); + } + + dispose(): void { + this._registration.dispose(); + } + + async provideTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + const data = CellUri.parseCellMetadataUri(resource); + // const data = parseCellUri(resource); + if (!data) { + return null; + } + + const ref = await this._notebookModelResolverService.resolve(data.notebook); + let result: ITextModel | null = null; + + const mode = this._modeService.create('json'); + + for (const cell of ref.object.notebook.cells) { + if (cell.handle === data.handle) { + const metadataSource = getFormatedMetadataJSON(ref.object.notebook, cell.metadata || {}, cell.language); + result = this._modelService.createModel( + metadataSource, + mode, + resource + ); + break; + } + } + + if (result) { + const once = result.onWillDispose(() => { + once.dispose(); + ref.dispose(); + }); + } + + return result; + } +} + class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution { constructor() { super(); @@ -596,6 +751,7 @@ class NotebookFileTracker implements IWorkbenchContribution { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(CellMetadataContentProvider, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); @@ -603,6 +759,7 @@ registerSingleton(INotebookService, NotebookService); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true); registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); +registerSingleton(INotebookEditorWidgetService, NotebookEditorWidgetService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index ebb3578105..f6ecdb29ad 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, ICellRange, IOrderedMimeType, ITransformedDisplayOutputDto, INotebookRendererInfo, IErrorOutput, IStreamOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -32,6 +32,7 @@ import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; +//#region Context Keys export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); // Is Notebook @@ -39,12 +40,16 @@ export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', ' // Editor keys export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +export const NOTEBOOK_EDITOR_OPEN = new RawContextKey('notebookEditorOpen', false); export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_EDITOR_RUNNABLE = new RawContextKey('notebookRunnable', true); export const NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK = new RawContextKey('notebookExecuting', false); +// Diff Editor Keys +export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); + // Cell keys export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown @@ -57,14 +62,114 @@ export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRu export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); // bool export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); // bool +// Kernels +export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); -// Shared commands +//#endregion + +//#region Shared commands export const EXPAND_CELL_CONTENT_COMMAND_ID = 'notebook.cell.expandCellContent'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; -// Kernels +//#endregion -export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); +//#region Output related types +export const enum RenderOutputType { + None, + Html, + Extension +} + +export interface IRenderNoOutput { + type: RenderOutputType.None; + hasDynamicHeight: boolean; +} + +export interface IRenderPlainHtmlOutput { + type: RenderOutputType.Html; + source: IDisplayOutputViewModel; + htmlContent: string; + hasDynamicHeight: boolean; +} + +export interface IRenderOutputViaExtension { + type: RenderOutputType.Extension; + source: IDisplayOutputViewModel; + mimeType: string; + renderer: INotebookRendererInfo; +} + +export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; +export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; + +export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; + + +export interface ICellOutputViewModel { + cellViewModel: IGenericCellViewModel; + model: IProcessedOutput; + isDisplayOutput(): this is IDisplayOutputViewModel; + isErrorOutput(): this is IErrorOutputViewModel; + isStreamOutput(): this is IStreamOutputViewModel; +} + +export interface IDisplayOutputViewModel extends ICellOutputViewModel { + model: ITransformedDisplayOutputDto; + resolveMimeTypes(textModel: NotebookTextModel): [readonly IOrderedMimeType[], number]; + pickedMimeType: number; +} + +export interface IErrorOutputViewModel extends ICellOutputViewModel { + model: IErrorOutput; +} + +export interface IStreamOutputViewModel extends ICellOutputViewModel { + model: IStreamOutput; +} + +//#endregion + +//#region Shared types between the Notebook Editor and Notebook Diff Editor, they are mostly used for output rendering + +export interface IGenericCellViewModel { + id: string; + handle: number; + uri: URI; + metadata: NotebookCellMetadata | undefined; + outputIsHovered: boolean; + outputsViewModels: ICellOutputViewModel[]; + getOutputOffset(index: number): number; + updateOutputHeight(index: number, height: number): void; +} + +export interface IDisplayOutputLayoutUpdateRequest { + output: IDisplayOutputViewModel; + cellTop: number; + outputOffset: number; +} + +export interface ICommonCellInfo { + cellId: string; + cellHandle: number; + cellUri: URI; +} + +export interface INotebookCellOutputLayoutInfo { + width: number; + height: number; + fontInfo: BareFontInfo; +} + +export interface ICommonNotebookEditor { + getCellOutputLayoutInfo(cell: IGenericCellViewModel): INotebookCellOutputLayoutInfo; + triggerScroll(event: IMouseWheelEvent): void; + getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; +} + +//#endregion export interface NotebookLayoutInfo { width: number; @@ -122,7 +227,7 @@ export interface MarkdownCellLayoutChangeEvent { totalHeight?: number; } -export interface ICellViewModel { +export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; readonly id: string; readonly textBuffer: IReadonlyTextBuffer; @@ -133,6 +238,7 @@ export interface ICellViewModel { cellKind: CellKind; editState: CellEditState; focusMode: CellFocusMode; + outputIsHovered: boolean; getText(): string; getTextLength(): number; metadata: NotebookCellMetadata | undefined; @@ -212,7 +318,7 @@ export interface IActiveNotebookEditor extends INotebookEditor { uri: URI; } -export interface INotebookEditor extends IEditor { +export interface INotebookEditor extends IEditor, ICommonNotebookEditor { isEmbedded: boolean; cursorNavigationMode: boolean; @@ -245,6 +351,7 @@ export interface INotebookEditor extends IEditor { getOverflowContainerDomNode(): HTMLElement; getInnerWebview(): Webview | undefined; getSelectionHandles(): number[]; + getSelectionViewModels(): ICellViewModel[]; /** * Focus the notebook editor cell list @@ -323,6 +430,8 @@ export interface INotebookEditor extends IEditor { */ focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; + /** * Execute the given notebook cell */ @@ -361,12 +470,12 @@ export interface INotebookEditor extends IEditor { /** * Remove the output from the webview layer */ - removeInset(output: IProcessedOutput): void; + removeInset(output: IDisplayOutputViewModel): void; /** * Hide the inset in the webview layer without removing it */ - hideInset(output: IProcessedOutput): void; + hideInset(output: IDisplayOutputViewModel): void; /** * Send message to the webview for outputs. @@ -395,11 +504,21 @@ export interface INotebookEditor extends IEditor { */ triggerScroll(event: IMouseWheelEvent): void; + /** + * The range will be revealed with as little scrolling as possible. + */ + revealCellRangeInView(range: ICellRange): void; + /** * Reveal cell into viewport. */ revealInView(cell: ICellViewModel): void; + /** + * Reveal cell into the top of viewport. + */ + revealInViewAtTop(cell: ICellViewModel): void; + /** * Reveal cell into viewport center. */ @@ -476,6 +595,9 @@ export interface INotebookEditor extends IEditor { * @return The contribution or null if contribution not found. */ getContribution(id: string): T; + + getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel; + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean): void; } export interface INotebookCellList { @@ -494,8 +616,8 @@ export interface INotebookCellList { scrollLeft: number; length: number; rowsContainer: HTMLElement; - readonly onDidRemoveOutput: Event; - readonly onDidHideOutput: Event; + readonly onDidRemoveOutput: Event; + readonly onDidHideOutput: Event; readonly onMouseUp: Event>; readonly onMouseDown: Event>; readonly onContextMenu: Event>; @@ -506,7 +628,9 @@ export interface INotebookCellList { focusElement(element: ICellViewModel): void; selectElement(element: ICellViewModel): void; getFocusedElements(): ICellViewModel[]; + revealElementsInView(range: ICellRange): void; revealElementInView(element: ICellViewModel): void; + revealElementInViewAtTop(element: ICellViewModel): void; revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; revealElementInCenter(element: ICellViewModel): void; revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise; @@ -594,7 +718,7 @@ export interface IOutputTransformContribution { * This call is allowed to have side effects, such as placing output * directly into the container element. */ - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; + render(output: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; } export interface CellFindMatch { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts index e3c15734f3..ccb3d64c19 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -9,7 +9,6 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookS import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { IReference } from 'vs/base/common/lifecycle'; @@ -69,9 +68,7 @@ export class NotebookDiffEditorInput extends EditorInput { public readonly options: NotebookEditorInputOptions, @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, - @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService, - // @IInstantiationService private readonly _instantiationService: IInstantiationService + @IFileDialogService private readonly _fileDialogService: IFileDialogService ) { super(); this._defaultDirtyState = !!options.startDirty; @@ -100,22 +97,6 @@ export class NotebookDiffEditorInput extends EditorInput { return false; } - isSaving(): boolean { - if (this.isUntitled()) { - return false; // untitled is never saving automatically - } - - if (!this.isDirty()) { - return false; // the editor needs to be dirty for being saved - } - - if (this._filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return true; // a short auto save is configured, treat this as being saved - } - - return false; - } - async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this._textModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 152a0ba19d..63812666f3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -20,13 +20,13 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -64,14 +64,7 @@ export class NotebookEditor extends EditorPane { this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); } - set viewModel(newModel: NotebookViewModel | undefined) { - if (this._widget.value) { - this._widget.value.viewModel = newModel; - this._onDidChangeModel.fire(); - } - } - - get viewModel() { + get viewModel(): NotebookViewModel | undefined { return this._widget.value?.viewModel; } @@ -134,17 +127,18 @@ export class NotebookEditor extends EditorPane { this._widget.value?.focus(); } + hasFocus(): boolean { + const activeElement = document.activeElement; + const value = this._widget.value; + + return !!value && (DOM.isAncestor(activeElement, (value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode())))); + } + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const group = this.group!; this._saveEditorViewState(this.input); - await super.setInput(input, options, context, token); - - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } this._widgetDisposableStore.clear(); @@ -155,11 +149,16 @@ export class NotebookEditor extends EditorPane { } this._widget = this.instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); + this._widgetDisposableStore.add(this._widget.value!.onDidChangeModel(() => this._onDidChangeModel.fire())); if (this._dimension) { this._widget.value!.layout(this._dimension, this._rootElement); } + // only now `setInput` and yield/await. this is AFTER the actual widget is ready. This is very important + // so that others synchronously receive a notebook editor with the correct widget being set + await super.setInput(input, options, context, token); + const model = await input.resolve(); // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index bea2bf768e..4f24be3b18 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -9,7 +9,6 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookS import { URI } from 'vs/base/common/uri'; import { isEqual, basename, joinPath } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { IReference } from 'vs/base/common/lifecycle'; @@ -36,7 +35,6 @@ export class NotebookEditorInput extends EditorInput { public readonly options: NotebookEditorInputOptions, @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, - @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, @IFileDialogService private readonly _fileDialogService: IFileDialogService, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { @@ -67,22 +65,6 @@ export class NotebookEditorInput extends EditorInput { return false; } - isSaving(): boolean { - if (this.isUntitled()) { - return false; // untitled is never saving automatically - } - - if (!this.isDirty()) { - return false; // the editor needs to be dirty for being saved - } - - if (this._filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return true; // a short auto save is configured, treat this as being saved - } - - return false; - } - async save(group: GroupIdentifier, options?: ISaveOptions): Promise { if (this._textModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 7be3b0a8d4..a9899d1235 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import * as strings from 'vs/base/common/strings'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -40,9 +40,8 @@ import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { 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, IDisplayOutputViewModel, 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 { 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'; @@ -55,13 +54,16 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookDecorationRenderOptions, INotebookKernelInfo2, IProcessedOutput, isTransformedDisplayOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, ICellRange, INotebookDecorationRenderOptions, INotebookKernelInfo2, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { configureKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { configureKernelIcon, errorStateIcon, successStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { extname } from 'vs/base/common/resources'; const $ = DOM.$; @@ -73,9 +75,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _overlayContainer!: HTMLElement; private _body!: HTMLElement; private _overflowContainer!: HTMLElement; - private _webview: BackLayerWebView | null = null; + private _webview: BackLayerWebView | null = null; private _webviewResolved: boolean = false; - private _webviewResolvePromise: Promise | null = null; + private _webviewResolvePromise: Promise | null> | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _list!: INotebookCellList; private _dndController: CellDragAndDropController | null = null; @@ -146,6 +148,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._notebookViewModel?.notebookDocument; } + private _activeKernelExecuted: boolean = false; private _activeKernel: INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; @@ -153,6 +156,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor readonly onDidChangeAvailableKernels: Event = this._onDidChangeAvailableKernels.event; private _contributedKernelsComputePromise: CancelablePromise | null = null; + private _initialKernelComputationDone: boolean = false; get activeKernel() { return this._activeKernel; @@ -175,9 +179,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._activeKernelResolvePromise = undefined; const memento = this._activeKernelMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - memento[this.viewModel.viewType] = this._activeKernel?.id; + memento[this.viewModel.viewType] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); this._onDidChangeKernel.fire(); + if (this._activeKernel) { + this._loadKernelPreloads(this._activeKernel.extensionLocation, this._activeKernel); + } } private _activeKernelResolvePromise: Promise | undefined = undefined; @@ -248,7 +255,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IThemeService private readonly themeService: IThemeService + @IThemeService private readonly themeService: IThemeService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(); this.isEmbedded = creationOptions.isEmbedded || false; @@ -292,6 +300,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this.viewModel?.selectionHandles || []; } + getSelectionViewModels(): ICellViewModel[] { + if (!this.viewModel) { + return []; + } + + return this.viewModel.selectionHandles.map(handle => this.viewModel!.getCellByHandle(handle)) as ICellViewModel[]; + } + hasModel(): this is IActiveNotebookEditor { return !!this._notebookViewModel; } @@ -428,7 +444,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _generateFontInfo(): void { const editorOptions = this.configurationService.getValue('editor'); - this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()); } private _createBody(parent: HTMLElement): void { @@ -467,7 +483,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor horizontalScrolling: false, keyboardSupport: false, mouseSupport: true, - multipleSelectionSupport: false, + multipleSelectionSupport: true, enableKeyboardNavigation: true, additionalScrollHeight: 0, transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', @@ -653,6 +669,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { this._detachModel(); await this._attachModel(textModel, viewState); + + type WorkbenchNotebookOpenClassification = { + scheme: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + viewType: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; + }; + + type WorkbenchNotebookOpenEvent = { + scheme: string; + ext: string; + viewType: string; + }; + + this.telemetryService.publicLog2('notebook/editorOpened', { + scheme: textModel.uri.scheme, + ext: extname(textModel.uri), + viewType: textModel.viewType + }); } else { this.restoreListViewState(viewState); } @@ -730,6 +764,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview?.element.remove(); this._webview = null; this._list.clear(); + this._activeKernel = undefined; + this._activeKernelExecuted = false; } async beginComputeContributedKernels() { @@ -742,6 +778,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); const result = await this._contributedKernelsComputePromise; + this._initialKernelComputationDone = true; this._contributedKernelsComputePromise = null; return result; @@ -752,6 +789,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (this._activeKernel !== undefined && this._activeKernelExecuted) { + // kernel already executed, we should not change it automatically + return; + } + const provider = this.notebookService.getContributedNotebookProvider(this.viewModel.viewType) || this.notebookService.getContributedNotebookProviders(this.viewModel.uri)[0]; const availableKernels = await this.beginComputeContributedKernels(); @@ -771,7 +813,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.multipleKernelsAvailable = false; } - const activeKernelStillExist = [...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); + const activeKernelStillExist = [...availableKernels].find(kernel => kernel.friendlyId === this.activeKernel?.friendlyId && this.activeKernel?.friendlyId !== undefined); if (activeKernelStillExist) { // the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost @@ -782,6 +824,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._setKernelsFromProviders(provider, availableKernels, tokenSource); } + this._initialKernelComputationDone = true; + tokenSource.dispose(); } @@ -797,7 +841,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cachedKernelId = memento[provider.id]; this.activeKernel = filteredKernels.find(kernel => kernel.isPreferred) - || filteredKernels.find(kernel => kernel.id === cachedKernelId) + || filteredKernels.find(kernel => kernel.friendlyId === cachedKernelId) || filteredKernels[0]; } else { this.activeKernel = undefined; @@ -818,7 +862,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - memento[provider.id] = this._activeKernel?.id; + memento[provider.id] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); tokenSource.dispose(); @@ -831,7 +875,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cachedKernelId = memento[provider.id]; const preferedKernel = kernelsFromSameExtension.find(kernel => kernel.isPreferred) - || kernelsFromSameExtension.find(kernel => kernel.id === cachedKernelId) + || kernelsFromSameExtension.find(kernel => kernel.friendlyId === cachedKernelId) || kernelsFromSameExtension[0]; this.activeKernel = preferedKernel; if (this.activeKernel) { @@ -848,7 +892,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - memento[provider.id] = this._activeKernel?.id; + memento[provider.id] = this._activeKernel?.friendlyId; this._activeKernelMemento.saveMemento(); tokenSource.dispose(); return; @@ -892,7 +936,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookExecuting?.set(notebookMetadata.runState === NotebookRunState.Running); } - private async _resolveWebview(): Promise { + private async _resolveWebview(): Promise | null> { if (!this.textModel) { return null; } @@ -902,7 +946,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } if (!this._webview) { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, this.getId(), this.textModel!.uri, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: CELL_OUTPUT_PADDING }); + this._webview.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; + this._webview.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; + // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); } @@ -950,7 +997,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async _createWebview(id: string, resource: URI): Promise { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { outputNodePadding: CELL_OUTPUT_PADDING, outputNodeLeftPadding: CELL_OUTPUT_PADDING }); + this._webview.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; + this._webview.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._webview.element); } @@ -1016,27 +1065,36 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webview!.element.style.height = `${scrollHeight}px`; if (this._webview?.insetMapping) { - const updateItems: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[] = []; - const removedItems: IProcessedOutput[] = []; + const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; + const removedItems: IDisplayOutputViewModel[] = []; this._webview?.insetMapping.forEach((value, key) => { - const cell = value.cell; + const cell = this.viewModel?.getCellByHandle(value.cellInfo.cellHandle); + if (!cell || !(cell instanceof CodeCellViewModel)) { + return; + } + + this.viewModel?.viewCells.find(cell => cell.handle === value.cellInfo.cellHandle); const viewIndex = this._list.getViewIndex(cell); if (viewIndex === undefined) { return; } - if (cell.outputs.indexOf(key) < 0) { + if (cell.outputsViewModels.indexOf(key) < 0) { // output is already gone removedItems.push(key); } const cellTop = this._list.getAbsoluteTopOfElement(cell); if (this._webview!.shouldUpdateInset(cell, key, cellTop)) { + const outputIndex = cell.outputsViewModels.indexOf(key); + + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); + updateItems.push({ - cell: cell, output: key, - cellTop: cellTop + cellTop: cellTop, + outputOffset }); } }); @@ -1212,10 +1270,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // this.viewModel!.selectionHandles = [cell.handle]; } + revealCellRangeInView(range: ICellRange) { + return this._list.revealElementsInView(range); + } + revealInView(cell: ICellViewModel) { this._list.revealElementInView(cell); } + revealInViewAtTop(cell: ICellViewModel) { + this._list.revealElementInViewAtTop(cell); + } + revealInCenterIfOutsideViewport(cell: ICellViewModel) { this._list.revealElementInCenterIfOutsideViewport(cell); } @@ -1421,6 +1487,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor language = this.viewModel.resolvedLanguages[0] || 'plaintext'; } } + + if (this.viewModel.resolvedLanguages.indexOf(language) < 0) { + // the language no longer exists + language = this.viewModel.resolvedLanguages[0] || 'plaintext'; + } } else { language = 'markdown'; } @@ -1627,26 +1698,37 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _ensureActiveKernel() { if (this._activeKernel) { - if (this._activeKernelResolvePromise) { - await this._activeKernelResolvePromise; - } - return; } + if (this._activeKernelResolvePromise) { + await this._activeKernelResolvePromise; + + if (this._activeKernel) { + return; + } + } + + + if (!this._initialKernelComputationDone) { + await this._setKernels(new CancellationTokenSource()); + + if (this._activeKernel) { + return; + } + } + // pick active kernel const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); picker.matchOnDetail = true; - picker.busy = true; - picker.show(); const tokenSource = new CancellationTokenSource(); const availableKernels = await this.beginComputeContributedKernels(); const picks: QuickPickInput[] = availableKernels.map((a) => { return { - id: a.id, + id: a.friendlyId, label: a.label, picked: false, description: @@ -1736,6 +1818,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } await this._ensureActiveKernel(); + this._activeKernelExecuted = true; await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, undefined); } @@ -1776,6 +1859,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } await this._ensureActiveKernel(); + this._activeKernelExecuted = true; await this._activeKernel?.executeNotebookCell!(this.viewModel.uri, cell.handle); } @@ -1818,6 +1902,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } + focusNextNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + const idx = this.viewModel?.getCellIndex(cell); + if (typeof idx !== 'number') { + return; + } + + const newCell = this.viewModel?.viewCells[idx + 1]; + if (!newCell) { + return; + } + + this.focusNotebookCell(newCell, focusItem); + } + //#endregion //#region MISC @@ -1842,12 +1940,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + getCellOutputLayoutInfo(cell: IGenericCellViewModel): INotebookCellOutputLayoutInfo { + if (!this._list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this._dimension!.width, + height: this._dimension!.height, + fontInfo: this._fontInfo! + }; + } + triggerScroll(event: IMouseWheelEvent) { this._list.triggerScrollFromMouseWheelEvent(event); } async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { - this._insetModifyQueueByOutputId.queue(output.source.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId, async () => { if (!this._webview) { return; } @@ -1856,22 +1966,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (!this._webview!.insetMapping.has(output.source)) { const cellTop = this._list.getAbsoluteTopOfElement(cell); - await this._webview!.createInset(cell, output, cellTop, offset); + await this._webview!.createInset({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); } else { const cellTop = this._list.getAbsoluteTopOfElement(cell); const scrollTop = this._list.scrollTop; + const outputIndex = cell.outputsViewModels.indexOf(output.source); + const outputOffset = cellTop + cell.getOutputOffset(outputIndex); - this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); + this._webview!.updateViewScrollTop(-scrollTop, true, [{ output: output.source, cellTop, outputOffset }]); } }); } - removeInset(output: IProcessedOutput) { - if (!isTransformedDisplayOutput(output)) { + removeInset(output: ICellOutputViewModel) { + if (!output.isDisplayOutput()) { return; } - this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (!this._webview || !this._webviewResolved) { return; } @@ -1879,16 +1991,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); } - hideInset(output: IProcessedOutput) { + hideInset(output: ICellOutputViewModel) { if (!this._webview || !this._webviewResolved) { return; } - if (!isTransformedDisplayOutput(output)) { + if (!output.isDisplayOutput()) { return; } - this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { this._webview!.hideInset(output); }); } @@ -1921,6 +2033,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._overlayContainer.classList.remove(className); } + getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel { + const { cellHandle } = cellInfo; + return this.viewModel?.viewCells.find(vc => vc.handle === cellHandle) as CodeCellViewModel; + } + + updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, outputHeight: number, isInit: boolean): void { + const cell = this.viewModel?.viewCells.find(vc => vc.handle === cellInfo.cellHandle); + if (cell && cell instanceof CodeCellViewModel) { + const outputIndex = cell.outputsViewModels.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + this.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); + } + } + //#endregion @@ -2012,6 +2138,13 @@ export const focusedCellBackground = registerColor('notebook.focusedCellBackgrou hc: null }, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); +export const selectedCellBackground = registerColor('notebook.selectedCellBackground', { + dark: null, + light: null, + hc: null +}, nls.localize('selectedCellBackground', "The background color of a cell when the cell is selected.")); + + export const cellHoverBackground = registerColor('notebook.cellHoverBackground', { dark: transparent(focusedCellBackground, .5), light: transparent(focusedCellBackground, .7), @@ -2022,7 +2155,7 @@ export const selectedCellBorder = registerColor('notebook.selectedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, hc: contrastBorder -}, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focusd.")); +}, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focused.")); export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { dark: focusBorder, @@ -2030,6 +2163,12 @@ export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { hc: focusBorder }, nls.localize('notebook.focusedCellBorder', "The color of the cell's top and bottom border when the cell is focused.")); +export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { + dark: notebookCellBorder, + light: notebookCellBorder, + hc: notebookCellBorder +}, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); + export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), dark: new Color(new RGBA(255, 255, 255, 0.15)), @@ -2042,7 +2181,6 @@ export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndic hc: focusBorder }, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); - export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { dark: scrollbarSliderBackground, light: scrollbarSliderBackground, @@ -2143,6 +2281,14 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-collapsed-part { background-color: ${focusedCellBackgroundColor} !important; }`); } + const selectedCellBackgroundColor = theme.getColor(selectedCellBackground); + if (selectedCellBackground) { + collector.addRule(`.notebookOverlay .monaco-list.selection-multiple .markdown-cell-row.selected { background-color: ${selectedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list.selection-multiple .code-cell-row.selected { background-color: ${selectedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list.selection-multiple .markdown-cell-row.selected .cell-focus-indicator-bottom { background-color: ${selectedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list.selection-multiple .code-cell-row.selected .cell-focus-indicator-bottom { background-color: ${selectedCellBackgroundColor} !important; }`); + } + const cellHoverBackgroundColor = theme.getColor(cellHoverBackground); if (cellHoverBackgroundColor) { collector.addRule(`.notebookOverlay .code-cell-row:not(.focused):hover .cell-focus-indicator, @@ -2161,6 +2307,15 @@ registerThemingParticipant((theme, collector) => { border-color: ${focusedCellBorderColor} !important; }`); + const inactiveFocusedBorderColor = theme.getColor(inactiveFocusedCellBorder); + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused .cell-inner-container:not(.cell-editor-focus):before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused .cell-inner-container:not(.cell-editor-focus):after { + border-color: ${inactiveFocusedBorderColor} !important; + }`); + const selectedCellBorderColor = theme.getColor(selectedCellBorder); collector.addRule(` .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, @@ -2191,12 +2346,12 @@ registerThemingParticipant((theme, collector) => { const cellStatusSuccessIcon = theme.getColor(cellStatusIconSuccess); if (cellStatusSuccessIcon) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: ${cellStatusSuccessIcon} }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status ${ThemeIcon.asCSSSelector(successStateIcon)} { color: ${cellStatusSuccessIcon} }`); } const cellStatusErrorIcon = theme.getColor(cellStatusIconError); if (cellStatusErrorIcon) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-error { color: ${cellStatusErrorIcon} }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status ${ThemeIcon.asCSSSelector(errorStateIcon)} { color: ${cellStatusErrorIcon} }`); } const cellStatusRunningIcon = theme.getColor(cellStatusIconRunning); @@ -2219,12 +2374,14 @@ registerThemingParticipant((theme, collector) => { if (scrollbarSliderBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider { background: ${editorBackgroundColor}; } `); collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderBackgroundColor}; } `); /* hack to not have cells see through scroller */ + // collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-track { background: ${scrollbarSliderBackgroundColor}; } `); } const scrollbarSliderHoverBackgroundColor = theme.getColor(listScrollbarSliderHoverBackground); if (scrollbarSliderHoverBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover { background: ${editorBackgroundColor}; } `); collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderHoverBackgroundColor}; } `); /* hack to not have cells see through scroller */ + collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { background: ${scrollbarSliderHoverBackgroundColor}; } `); } const scrollbarSliderActiveBackgroundColor = theme.getColor(listScrollbarSliderActiveBackground); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index 3f0f7857da..bff663bd5d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -3,14 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceMap } from 'vs/base/common/map'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupChangeKind, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IInstantiationService, createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const INotebookEditorWidgetService = createDecorator('INotebookEditorWidgetService'); @@ -20,139 +16,6 @@ export interface IBorrowValue { export interface INotebookEditorWidgetService { _serviceBrand: undefined; + widgets: NotebookEditorWidget[]; retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue; } - -class NotebookEditorWidgetService implements INotebookEditorWidgetService { - - readonly _serviceBrand: undefined; - - private _tokenPool = 1; - - private readonly _notebookWidgets = new Map>(); - private readonly _disposables = new DisposableStore(); - - constructor( - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService, - ) { - - const groupListener = new Map(); - const onNewGroup = (group: IEditorGroup) => { - const { id } = group; - const listener = group.onDidGroupChange(e => { - const widgets = this._notebookWidgets.get(group.id); - if (!widgets || e.kind !== GroupChangeKind.EDITOR_CLOSE || !(e.editor instanceof NotebookEditorInput)) { - return; - } - const value = widgets.get(e.editor.resource); - if (!value) { - return; - } - value.token = undefined; - this._disposeWidget(value.widget); - widgets.delete(e.editor.resource); - }); - groupListener.set(id, listener); - }; - this._disposables.add(editorGroupService.onDidAddGroup(onNewGroup)); - editorGroupService.groups.forEach(onNewGroup); - - // group removed -> clean up listeners, clean up widgets - this._disposables.add(editorGroupService.onDidRemoveGroup(group => { - const listener = groupListener.get(group.id); - if (listener) { - listener.dispose(); - groupListener.delete(group.id); - } - const widgets = this._notebookWidgets.get(group.id); - this._notebookWidgets.delete(group.id); - if (widgets) { - for (const value of widgets.values()) { - value.token = undefined; - this._disposeWidget(value.widget); - } - } - })); - - // HACK - // we use the open override to spy on tab movements because that's the only - // way to do that... - this._disposables.add(editorService.overrideOpenEditor({ - open: (input, _options, group, context) => { - if (input instanceof NotebookEditorInput && context === OpenEditorContext.MOVE_EDITOR) { - // when moving a notebook editor we release it from its current tab and we - // "place" it into its future slot so that the editor can pick it up from there - this._freeWidget(input, editorGroupService.activeGroup, group); - } - return undefined; - } - })); - } - - private _disposeWidget(widget: NotebookEditorWidget): void { - widget.onWillHide(); - const domNode = widget.getDomNode(); - widget.dispose(); - domNode.remove(); - } - - private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { - const targetWidget = this._notebookWidgets.get(target.id)?.get(input.resource); - if (targetWidget) { - // not needed - return; - } - - const widget = this._notebookWidgets.get(source.id)?.get(input.resource); - if (!widget) { - throw new Error('no widget at source group'); - } - this._notebookWidgets.get(source.id)?.delete(input.resource); - widget.token = undefined; - - let targetMap = this._notebookWidgets.get(target.id); - if (!targetMap) { - targetMap = new ResourceMap(); - this._notebookWidgets.set(target.id, targetMap); - } - targetMap.set(input.resource, widget); - } - - retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue { - - let value = this._notebookWidgets.get(group.id)?.get(input.resource); - - if (!value) { - // NEW widget - const instantiationService = accessor.get(IInstantiationService); - const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); - const token = this._tokenPool++; - value = { widget, token }; - - let map = this._notebookWidgets.get(group.id); - if (!map) { - map = new ResourceMap(); - this._notebookWidgets.set(group.id, map); - } - map.set(input.resource, value); - - } else { - // reuse a widget which was either free'ed before or which - // is simply being reused... - value.token = this._tokenPool++; - } - - return this._createBorrowValue(value.token!, value); - } - - private _createBorrowValue(myToken: number, widget: { widget: NotebookEditorWidget, token: number | undefined }): IBorrowValue { - return { - get value() { - return widget.token === myToken ? widget.widget : undefined; - } - }; - } -} - -registerSingleton(INotebookEditorWidgetService, NotebookEditorWidgetService, true); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts new file mode 100644 index 0000000000..f375a6fab0 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetServiceImpl.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceMap } from 'vs/base/common/map'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; + +export class NotebookEditorWidgetService implements INotebookEditorWidgetService { + + readonly _serviceBrand: undefined; + + private _tokenPool = 1; + + private readonly _notebookWidgets = new Map>(); + private readonly _disposables = new DisposableStore(); + + get widgets() { + return [...this._notebookWidgets.values()].map(val => [...val.values()].map(widget => widget.widget)).reduce((prev, curr) => { return [...prev, ...curr]; }, []); + } + + constructor( + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService, + ) { + + const groupListener = new Map(); + const onNewGroup = (group: IEditorGroup) => { + const { id } = group; + const listener = group.onDidGroupChange(e => { + const widgets = this._notebookWidgets.get(group.id); + if (!widgets || e.kind !== GroupChangeKind.EDITOR_CLOSE || !(e.editor instanceof NotebookEditorInput)) { + return; + } + const value = widgets.get(e.editor.resource); + if (!value) { + return; + } + value.token = undefined; + this._disposeWidget(value.widget); + widgets.delete(e.editor.resource); + }); + groupListener.set(id, listener); + }; + this._disposables.add(editorGroupService.onDidAddGroup(onNewGroup)); + editorGroupService.groups.forEach(onNewGroup); + + // group removed -> clean up listeners, clean up widgets + this._disposables.add(editorGroupService.onDidRemoveGroup(group => { + const listener = groupListener.get(group.id); + if (listener) { + listener.dispose(); + groupListener.delete(group.id); + } + const widgets = this._notebookWidgets.get(group.id); + this._notebookWidgets.delete(group.id); + if (widgets) { + for (const value of widgets.values()) { + value.token = undefined; + this._disposeWidget(value.widget); + } + } + })); + + // HACK + // we use the open override to spy on tab movements because that's the only + // way to do that... + this._disposables.add(editorService.overrideOpenEditor({ + open: (input, _options, group, context) => { + if (input instanceof NotebookEditorInput && context === OpenEditorContext.MOVE_EDITOR) { + // when moving a notebook editor we release it from its current tab and we + // "place" it into its future slot so that the editor can pick it up from there + this._freeWidget(input, editorGroupService.activeGroup, group); + } + return undefined; + } + })); + } + + private _disposeWidget(widget: NotebookEditorWidget): void { + widget.onWillHide(); + const domNode = widget.getDomNode(); + widget.dispose(); + domNode.remove(); + } + + private _freeWidget(input: NotebookEditorInput, source: IEditorGroup, target: IEditorGroup): void { + const targetWidget = this._notebookWidgets.get(target.id)?.get(input.resource); + if (targetWidget) { + // not needed + return; + } + + const widget = this._notebookWidgets.get(source.id)?.get(input.resource); + if (!widget) { + throw new Error('no widget at source group'); + } + this._notebookWidgets.get(source.id)?.delete(input.resource); + widget.token = undefined; + + let targetMap = this._notebookWidgets.get(target.id); + if (!targetMap) { + targetMap = new ResourceMap(); + this._notebookWidgets.set(target.id, targetMap); + } + targetMap.set(input.resource, widget); + } + + retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput): IBorrowValue { + + let value = this._notebookWidgets.get(group.id)?.get(input.resource); + + if (!value) { + // NEW widget + const instantiationService = accessor.get(IInstantiationService); + const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); + const token = this._tokenPool++; + value = { widget, token }; + + let map = this._notebookWidgets.get(group.id); + if (!map) { + map = new ResourceMap(); + this._notebookWidgets.set(group.id, map); + } + map.set(input.resource, value); + + } else { + // reuse a widget which was either free'ed before or which + // is simply being reused... + value.token = this._tokenPool++; + } + + return this._createBorrowValue(value.token!, value); + } + + private _createBorrowValue(myToken: number, widget: { widget: NotebookEditorWidget, token: number | undefined }): IBorrowValue { + return { + get value() { + return widget.token === myToken ? widget.widget : undefined; + } + }; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index 81d08906cd..c28e8ff0c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -7,7 +7,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { localize } from 'vs/nls'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -export const configureKernelIcon = registerIcon('notebook-kernel-configure', Codicon.settingsGear, localize('configureKernel', 'Configure icon in kernel configuation widget in notebook editors.')); +export const configureKernelIcon = registerIcon('notebook-kernel-configure', Codicon.settingsGear, localize('configureKernel', 'Configure icon in kernel configuration widget in notebook editors.')); export const selectKernelIcon = registerIcon('notebook-kernel-select', Codicon.serverEnvironment, localize('selectKernelIcon', 'Configure icon to select a kernel in notebook editors.')); export const executeIcon = registerIcon('notebook-execute', Codicon.play, localize('executeIcon', 'Icon to execute in notebook editors.')); @@ -16,17 +16,18 @@ export const deleteCellIcon = registerIcon('notebook-delete-cell', Codicon.trash export const executeAllIcon = registerIcon('notebook-execute-all', Codicon.runAll, localize('executeAllIcon', 'Icon to execute all cells in notebook editors.')); export const editIcon = registerIcon('notebook-edit', Codicon.pencil, localize('editIcon', 'Icon to edit a cell in notebook editors.')); export const stopEditIcon = registerIcon('notebook-stop-edit', Codicon.check, localize('stopEditIcon', 'Icon to stop editing a cell in notebook editors.')); -export const moveUpIcon = registerIcon('notebook-move-up', Codicon.arrowUp, localize('moveUpIcon', 'Icon to move a cell up in notebook editors.')); -export const moveDownIcon = registerIcon('notebook-move-down', Codicon.arrowDown, localize('moveDownIcon', 'Icon to move a cell down in notebook editors.')); +export const moveUpIcon = registerIcon('notebook-move-up', Codicon.arrowUp, localize('moveUpIcon', 'Icon to move up a cell in notebook editors.')); +export const moveDownIcon = registerIcon('notebook-move-down', Codicon.arrowDown, localize('moveDownIcon', 'Icon to move down a cell in notebook editors.')); export const clearIcon = registerIcon('notebook-clear', Codicon.clearAll, localize('clearIcon', 'Icon to clear cell outputs in notebook editors.')); export const splitCellIcon = registerIcon('notebook-split-cell', Codicon.splitVertical, localize('splitCellIcon', 'Icon to split a cell in notebook editors.')); export const unfoldIcon = registerIcon('notebook-unfold', Codicon.unfold, localize('unfoldIcon', 'Icon to unfold a cell in notebook editors.')); export const successStateIcon = registerIcon('notebook-state-success', Codicon.check, localize('successStateIcon', 'Icon to indicate a success state in notebook editors.')); -export const errorStateIcon = registerIcon('notebook-state-error', Codicon.error, localize('errorStateIcon', 'Icon to indicate a error state in notebook editors.')); +export const errorStateIcon = registerIcon('notebook-state-error', Codicon.error, localize('errorStateIcon', 'Icon to indicate an error state in notebook editors.')); -export const collapsedIcon = registerIcon('notebook-collapsed', Codicon.chevronRight, localize('collapsedIcon', 'Icon to annotated a collapsed section in notebook editors.')); -export const expandedIcon = registerIcon('notebook-expanded', Codicon.chevronDown, localize('expandedIcon', 'Icon to annotated a expanded section in notebook editors.')); +export const collapsedIcon = registerIcon('notebook-collapsed', Codicon.chevronRight, localize('collapsedIcon', 'Icon to annotate a collapsed section in notebook editors.')); +export const expandedIcon = registerIcon('notebook-expanded', Codicon.chevronDown, localize('expandedIcon', 'Icon to annotate an expanded section in notebook editors.')); export const openAsTextIcon = registerIcon('notebook-open-as-text', Codicon.fileCode, localize('openAsTextIcon', 'Icon to open the notebook in a text editor.')); export const revertIcon = registerIcon('notebook-revert', Codicon.discard, localize('revertIcon', 'Icon to revert in notebook editors.')); -export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type notebook editors.')); +export const renderOutputIcon = registerIcon('notebook-render-output', Codicon.preview, localize('renderOutputIcon', 'Icon to render output in diff editor.')); +export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts index 8362c71c67..9e9e31dc13 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts @@ -5,9 +5,9 @@ import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -export type IOutputTransformCtor = IConstructorSignature1; +export type IOutputTransformCtor = IConstructorSignature1; export interface IOutputTransformDescription { id: string; @@ -20,7 +20,7 @@ export const NotebookRegistry = new class NotebookRegistryImpl { readonly outputTransforms: IOutputTransformDescription[] = []; - registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void { + registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: ICommonNotebookEditor, ...services: Services): IOutputTransformContribution }): void { this.outputTransforms.push({ id: id, kind: kind, ctor: ctor as IOutputTransformCtor }); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 1643a86fd2..10fa7d66b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; +import { getPixelRatio, getZoomLevel } from 'vs/base/browser/browser'; import { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -27,12 +27,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Memento } from 'vs/workbench/common/memento'; import { INotebookEditorContribution, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; -import { CellEditState, getActiveNotebookEditor, INotebookEditor, NotebookEditorOptions, updateEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, getActiveNotebookEditor, ICellViewModel, INotebookEditor, NotebookEditorOptions, updateEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRegistry, updateNotebookKernelProvideAssociationSchema } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellKind, CellOutputKind, DisplayOrderKey, IDisplayOutput, INotebookDecorationRenderOptions, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, RENDERER_NOT_AVAILABLE, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellKind, CellOutputKind, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookDecorationRenderOptions, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, RENDERER_NOT_AVAILABLE, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -65,6 +65,10 @@ export class NotebookKernelProviderInfoStore extends Disposable { return this._notebookKernelProviders.filter(provider => notebookDocumentFilterMatch(provider.selector, viewType, resource)); } + getContributedKernelProviders() { + return [...this._notebookKernelProviders.values()]; + } + private _updateProviderExtensionsInfo() { NotebookKernelProviderAssociationRegistry.extensionIds.length = 0; NotebookKernelProviderAssociationRegistry.extensionDescriptions.length = 0; @@ -237,7 +241,7 @@ class ModelData implements IDisposable { } export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { declare readonly _serviceBrand: undefined; - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); @@ -266,12 +270,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu private readonly _onDidChangeKernels = new Emitter(); onDidChangeKernels: Event = this._onDidChangeKernels.event; - private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>(); - onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }> = this._onDidChangeNotebookActiveKernel.event; + private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }>(); + onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }> = this._onDidChangeNotebookActiveKernel.event; private cutItems: NotebookCellTextModel[] | undefined; private _lastClipboardIsCopy: boolean = true; - private _displayOrder: { userOrder: string[], defaultOrder: string[] } = Object.create(null); + private _displayOrder: { userOrder: string[], defaultOrder: string[]; } = Object.create(null); private readonly _decorationOptionProviders = new Map(); constructor( @@ -366,7 +370,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu // there is a `::before` or `::after` text decoration whose position is above or below current line // we at least make sure that the editor top padding is at least one line const editorOptions = this.configurationService.getValue('editor'); - updateEditorTopPadding(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight + 2); + updateEditorTopPadding(BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel(), getPixelRatio()).lineHeight + 2); decorationTriggeredAdjustment = true; break; } @@ -387,7 +391,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu }; }; - const PRIORITY = 50; + const PRIORITY = 105; this._register(UndoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { @@ -433,8 +437,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } - const { editor, activeCell } = getContext(); - if (!editor || !activeCell) { + const { editor } = getContext(); + if (!editor) { return false; } @@ -445,8 +449,14 @@ export class NotebookService extends Disposable implements INotebookService, ICu const clipboardService = accessor.get(IClipboardService); const notebookService = accessor.get(INotebookService); - clipboardService.writeText(activeCell.getText()); - notebookService.setToCopy([activeCell.model], true); + const selectedCells = editor.getSelectionViewModels(); + + if (!selectedCells.length) { + return false; + } + + clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n')); + notebookService.setToCopy(selectedCells.map(cell => cell.model), true); return true; })); @@ -569,8 +579,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } - const { editor, activeCell } = getContext(); - if (!editor || !activeCell) { + const { editor } = getContext(); + if (!editor) { return false; } @@ -586,9 +596,20 @@ export class NotebookService extends Disposable implements INotebookService, ICu const clipboardService = accessor.get(IClipboardService); const notebookService = accessor.get(INotebookService); - clipboardService.writeText(activeCell.getText()); - viewModel.deleteCell(viewModel.getCellIndex(activeCell), true); - notebookService.setToCopy([activeCell.model], false); + const selectedCells = editor.getSelectionViewModels(); + + if (!selectedCells.length) { + return false; + } + + clipboardService.writeText(selectedCells.map(cell => cell.getText()).join('\n')); + + const edits: ICellEditOperation[] = selectedCells.map(cell => [cell, viewModel.getCellIndex(cell)] as [ICellViewModel, number]).sort((a, b) => b[1] - a[1]).map(value => { + return { editType: CellEditType.Replace, index: value[1], count: 1, cells: [] }; + }); + + viewModel.notebookDocument.applyEdits(viewModel.notebookDocument.versionId, edits, true, editor.getSelectionHandles(), () => { return undefined; }, undefined, true); + notebookService.setToCopy(selectedCells.map(cell => cell.model), false); return true; }); @@ -623,6 +644,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu } async canResolve(viewType: string): Promise { + await this._extensionService.activateByEvent(`onNotebook:*`); + if (!this._notebookProviders.has(viewType)) { await this._extensionService.whenInstalledExtensionsRegistered(); // this awaits full activation of all matching extensions @@ -691,9 +714,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu const data = await provider.provideKernels(resource, token); result[index] = data.map(dto => { return { + id: dto.id, extension: dto.extension, extensionLocation: URI.revive(dto.extensionLocation), - id: dto.id, + friendlyId: dto.friendlyId, label: dto.label, description: dto.description, detail: dto.detail, @@ -701,13 +725,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu preloads: dto.preloads, providerHandle: dto.providerHandle, resolve: async (uri: URI, editorId: string, token: CancellationToken) => { - return provider.resolveKernel(editorId, uri, dto.id, token); + return provider.resolveKernel(editorId, uri, dto.friendlyId, token); }, executeNotebookCell: async (uri: URI, handle: number | undefined) => { - return provider.executeNotebook(uri, dto.id, handle); + return provider.executeNotebook(uri, dto.friendlyId, handle); }, cancelNotebookCell: (uri: URI, handle: number | undefined): Promise => { - return provider.cancelNotebook(uri, dto.id, handle); + return provider.cancelNotebook(uri, dto.friendlyId, handle); } }; }); @@ -718,6 +742,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu return flatten(result); } + async getContributedNotebookKernelProviders(): Promise { + const kernelProviders = this.notebookKernelProviderInfoStore.getContributedKernelProviders(); + return kernelProviders; + } + getRendererInfo(id: string): INotebookRendererInfo | undefined { return this.notebookRenderersInfoStore.get(id); } @@ -890,7 +919,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu listVisibleNotebookEditors(): INotebookEditor[] { return this._editorService.visibleEditorPanes - .filter(pane => (pane as unknown as { isNotebookEditor?: boolean }).isNotebookEditor) + .filter(pane => (pane as unknown as { isNotebookEditor?: boolean; }).isNotebookEditor) .map(pane => pane.getControl() as INotebookEditor) .filter(editor => !!editor) .filter(editor => this._notebookEditors.has(editor.getId())); @@ -912,7 +941,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeNotebookActiveKernel.fire({ uri: editor.uri!, providerHandle: editor.activeKernel?.providerHandle, - kernelId: editor.activeKernel?.id + kernelFriendlyId: editor.activeKernel?.friendlyId }); })); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 0db68a1a32..32adb4a998 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange, NOTEBOOK_EDITOR_CURSOR_BEGIN_END } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -45,10 +45,10 @@ export class NotebookCellList extends WorkbenchList implements ID private _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; - private readonly _onDidRemoveOutput = new Emitter(); - readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; - private readonly _onDidHideOutput = new Emitter(); - readonly onDidHideOutput: Event = this._onDidHideOutput.event; + private readonly _onDidRemoveOutput = new Emitter(); + readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; + private readonly _onDidHideOutput = new Emitter(); + readonly onDidHideOutput: Event = this._onDidHideOutput.event; private _viewModel: NotebookViewModel | null = null; private _hiddenRangeIds: string[] = []; @@ -314,15 +314,17 @@ export class NotebookCellList extends WorkbenchList implements ID if (e.synchronous) { viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -338,15 +340,17 @@ export class NotebookCellList extends WorkbenchList implements ID } viewDiffs.reverse().forEach((diff) => { - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -364,7 +368,7 @@ export class NotebookCellList extends WorkbenchList implements ID const viewSelections = model.selectionHandles.map(handle => { return model.getCellByHandle(handle); }).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); - this.setFocus(viewSelections, undefined, true); + this.setSelection(viewSelections, undefined, true); })); const hiddenRanges = model.getHiddenRanges(); @@ -460,15 +464,17 @@ export class NotebookCellList extends WorkbenchList implements ID viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IProcessedOutput[] = []; - const deletedOutputs: IProcessedOutput[] = []; + const hideOutputs: ICellOutputViewModel[] = []; + const deletedOutputs: ICellOutputViewModel[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell.handle)) { - hideOutputs.push(...cell?.model.outputs); - } else { - deletedOutputs.push(...cell?.model.outputs); + if (cell.cellKind === CellKind.Code) { + if (this._viewModel!.hasCell(cell.handle)) { + hideOutputs.push(...cell?.outputsViewModels); + } else { + deletedOutputs.push(...cell?.outputsViewModels); + } } } @@ -541,6 +547,22 @@ export class NotebookCellList extends WorkbenchList implements ID return viewIndexInfo.index; } + private _getViewIndexUpperBound2(modelIndex: number) { + if (!this.hiddenRangesPrefixSum) { + return modelIndex; + } + + const viewIndexInfo = this.hiddenRangesPrefixSum.getIndexOf(modelIndex); + + if (viewIndexInfo.remainder !== 0) { + if (modelIndex >= this.hiddenRangesPrefixSum.getTotalValue()) { + return modelIndex - (this.hiddenRangesPrefixSum.getTotalValue() - this.hiddenRangesPrefixSum.getCount()); + } + } + + return viewIndexInfo.index; + } + focusElement(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -570,6 +592,18 @@ export class NotebookCellList extends WorkbenchList implements ID } setFocus(indexes: number[], browserEvent?: UIEvent, ignoreTextModelUpdate?: boolean): void { + // if (!indexes.length) { + // return; + // } + + // if (this._viewModel && !ignoreTextModelUpdate) { + // this._viewModel.selectionHandles = indexes.map(index => this.element(index)).map(cell => cell.handle); + // } + + super.setFocus(indexes, browserEvent); + } + + setSelection(indexes: number[], browserEvent?: UIEvent | undefined, ignoreTextModelUpdate?: boolean) { if (!indexes.length) { return; } @@ -578,7 +612,47 @@ export class NotebookCellList extends WorkbenchList implements ID this._viewModel.selectionHandles = indexes.map(index => this.element(index)).map(cell => cell.handle); } - super.setFocus(indexes, browserEvent); + super.setSelection(indexes, browserEvent); + } + + revealElementsInView(range: ICellRange) { + const startIndex = this._getViewIndexUpperBound2(range.start); + + if (startIndex < 0) { + return; + } + + const endIndex = this._getViewIndexUpperBound2(range.end); + + const scrollTop = this.getViewScrollTop(); + const wrapperBottom = this.getViewScrollBottom(); + const elementTop = this.view.elementTop(startIndex); + if (elementTop >= scrollTop + && elementTop < wrapperBottom) { + // start element is visible + // check end + + const endElementTop = this.view.elementTop(endIndex); + const endElementHeight = this.view.elementHeight(endIndex); + + if (endElementTop >= wrapperBottom) { + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + + if (endElementTop < wrapperBottom) { + // end element partially visible + if (endElementTop + endElementHeight - wrapperBottom < elementTop - scrollTop) { + // there is enough space to just scroll up a little bit to make the end element visible + return this.view.setScrollTop(scrollTop + endElementTop + endElementHeight - wrapperBottom); + } else { + // don't even try it + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + } + } + + + this._revealInView(startIndex); } revealElementInView(cell: ICellViewModel) { @@ -589,6 +663,14 @@ export class NotebookCellList extends WorkbenchList implements ID } } + revealElementInViewAtTop(cell: ICellViewModel) { + const index = this._getViewIndexUpperBound(cell); + + if (index >= 0) { + this._revealInternal(index, false, CellRevealPosition.Top); + } + } + revealElementInCenterIfOutsideViewport(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 034b83472e..5015a04178 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellOutputViewModel, ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { URI } from 'vs/base/common/uri'; export class OutputRenderer { @@ -15,7 +14,7 @@ export class OutputRenderer { protected readonly _mimeTypeMapping: { [key: number]: IOutputTransformContribution; }; constructor( - notebookEditor: INotebookEditor, + notebookEditor: ICommonNotebookEditor, private readonly instantiationService: IInstantiationService ) { this._contributions = {}; @@ -34,7 +33,8 @@ export class OutputRenderer { } } - renderNoop(output: IProcessedOutput, container: HTMLElement): IRenderOutput { + renderNoop(viewModel: ICellOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const contentNode = document.createElement('p'); contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; @@ -42,13 +42,14 @@ export class OutputRenderer { return { type: RenderOutputType.None, hasDynamicHeight: false }; } - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + render(viewModel: ICellOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { + const output = viewModel.model; const transform = this._mimeTypeMapping[output.outputKind]; if (transform) { - return transform.render(output, container, preferredMimeType, notebookUri); + return transform.render(viewModel, container, preferredMimeType, notebookUri); } else { - return this.renderNoop(output, container); + return this.renderNoop(viewModel, container); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts index 93235d300f..f2c07d4918 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts @@ -3,21 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, IErrorOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { RGBA, Color } from 'vs/base/common/color'; import { ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IErrorOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -class ErrorTransform implements IOutputTransformContribution { +export class ErrorTransform implements IOutputTransformContribution { constructor( - public editor: INotebookEditor, + _editor: ICommonNotebookEditor, @IThemeService private readonly themeService: IThemeService ) { } - render(output: IErrorOutput, container: HTMLElement): IRenderOutput { + render(viewModel: IErrorOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; const header = document.createElement('div'); const headerMessage = output.ename && output.evalue ? `${output.ename}: ${output.evalue}` diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index eb7a67efdf..9118f51825 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IDisplayOutputViewModel, IOutputTransformContribution, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { isArray } from 'vs/base/common/types'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -20,12 +20,13 @@ import { dirname } from 'vs/base/common/resources'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ErrorTransform } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; class RichRenderer implements IOutputTransformContribution { - private _richMimeTypeRenderers = new Map IRenderOutput>(); + private _richMimeTypeRenderers = new Map IRenderOutput>(); constructor( - public notebookEditor: INotebookEditor, + public notebookEditor: ICommonNotebookEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService, @@ -42,10 +43,11 @@ class RichRenderer implements IOutputTransformContribution { this._richMimeTypeRenderers.set('image/jpeg', this.renderJPEG.bind(this)); this._richMimeTypeRenderers.set('text/plain', this.renderPlainText.bind(this)); this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); + this._richMimeTypeRenderers.set('application/x.notebook.error-traceback', this._renderErrorTraceback.bind(this)); } - render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { - if (!output.data) { + render(output: IDisplayOutputViewModel, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { + if (!output.model.data) { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; container.appendChild(contentNode); @@ -55,7 +57,7 @@ class RichRenderer implements IOutputTransformContribution { if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { const contentNode = document.createElement('p'); const mimeTypes = []; - for (const property in output.data) { + for (const property in output.model.data) { mimeTypes.push(property); } @@ -75,8 +77,8 @@ class RichRenderer implements IOutputTransformContribution { return renderer!(output, notebookUri, container); } - renderJSON(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['application/json']; + renderJSON(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['application/json']; const str = JSON.stringify(data, null, '\t'); const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { @@ -94,8 +96,8 @@ class RichRenderer implements IOutputTransformContribution { const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - const width = this.notebookEditor.getLayoutInfo().width; - const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; + const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ @@ -108,8 +110,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderCode(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/x-javascript']; + renderCode(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/x-javascript']; const str = (isArray(data) ? data.join('') : data) as string; const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { @@ -127,8 +129,8 @@ class RichRenderer implements IOutputTransformContribution { const textModel = this.modelService.createModel(str, mode, resource, false); editor.setModel(textModel); - const width = this.notebookEditor.getLayoutInfo().width; - const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const width = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).width; + const fontInfo = this.notebookEditor.getCellOutputLayoutInfo(output.cellViewModel).fontInfo; const height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); editor.layout({ @@ -141,8 +143,8 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJavaScript(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['application/javascript']; + renderJavaScript(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['application/javascript']; const str = isArray(data) ? data.join('') : data; const scriptVal = ``; return { @@ -153,8 +155,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderHTML(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/html']; + renderHTML(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/html']; const str = (isArray(data) ? data.join('') : data) as string; return { type: RenderOutputType.Html, @@ -164,8 +166,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderSVG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['image/svg+xml']; + renderSVG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['image/svg+xml']; const str = (isArray(data) ? data.join('') : data) as string; return { type: RenderOutputType.Html, @@ -175,8 +177,8 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderMarkdown(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/markdown']; + renderMarkdown(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/markdown']; const str = (isArray(data) ? data.join('') : data) as string; const mdOutput = document.createElement('div'); const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); @@ -186,9 +188,9 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPNG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { + renderPNG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); - image.src = `data:image/png;base64,${output.data['image/png']}`; + image.src = `data:image/png;base64,${output.model.data['image/png']}`; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -196,9 +198,9 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJPEG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { + renderJPEG(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); - image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; + image.src = `data:image/jpeg;base64,${output.model.data['image/jpeg']}`; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -206,15 +208,23 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPlainText(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { - const data = output.data['text/plain']; + renderPlainText(output: IDisplayOutputViewModel, notebookUri: URI, container: HTMLElement): IRenderOutput { + const data = output.model.data['text/plain']; const contentNode = DOM.$('.output-plaintext'); - truncatedArrayOfString(contentNode, isArray(data) ? data : [data], this.openerService, this.textFileService, this.themeService, true); + truncatedArrayOfString(contentNode, isArray(data) ? data : [data], this.openerService, this.textFileService, this.themeService); container.appendChild(contentNode); return { type: RenderOutputType.None, hasDynamicHeight: false }; } + private _renderErrorTraceback(outputViewModel: IDisplayOutputViewModel, _notebookUri: URI, container: HTMLElement): IRenderOutput { + const output = outputViewModel.model.data['application/x.notebook.error-traceback'] as any; + const transform = new ErrorTransform(this.notebookEditor, this.themeService); + transform.render(output, container); + transform.dispose(); + return { type: RenderOutputType.None, hasDynamicHeight: false }; + } + dispose(): void { } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts index 0642f79c10..73ee016e51 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { IRenderOutput, CellOutputKind, IStreamOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; -import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICommonNotebookEditor, IOutputTransformContribution, IRenderOutput, IStreamOutputViewModel, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -14,16 +14,17 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; class StreamRenderer implements IOutputTransformContribution { constructor( - editor: INotebookEditor, + editor: ICommonNotebookEditor, @IOpenerService readonly openerService: IOpenerService, @ITextFileService readonly textFileService: ITextFileService, @IThemeService readonly themeService: IThemeService ) { } - render(output: IStreamOutput, container: HTMLElement): IRenderOutput { - const contentNode = DOM.$('.output-stream'); - truncatedArrayOfString(contentNode, [output.text], this.openerService, this.textFileService, this.themeService, false); + render(viewModel: IStreamOutputViewModel, container: HTMLElement): IRenderOutput { + const output = viewModel.model; + const contentNode = DOM.$('span.output-stream'); + truncatedArrayOfString(contentNode, [output.text], this.openerService, this.textFileService, this.themeService); container.appendChild(contentNode); return { type: RenderOutputType.None, hasDynamicHeight: false }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts index 9a0133b1d0..5fe11e8a25 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts @@ -49,7 +49,7 @@ function generateViewMoreElement(outputs: string[], openerService: IOpenerServic return element; } -export function truncatedArrayOfString(container: HTMLElement, outputs: string[], openerService: IOpenerService, textFileService: ITextFileService, themeService: IThemeService, renderANSI: boolean) { +export function truncatedArrayOfString(container: HTMLElement, outputs: string[], openerService: IOpenerService, textFileService: ITextFileService, themeService: IThemeService) { const fullLen = outputs.reduce((p, c) => { return p + c.length; }, 0); @@ -61,18 +61,11 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const bufferBuilder = new PieceTreeTextBufferBuilder(); outputs.forEach(output => bufferBuilder.acceptChunk(output)); const factory = bufferBuilder.finish(); - buffer = factory.create(DefaultEndOfLine.LF); + buffer = factory.create(DefaultEndOfLine.LF).textBuffer; const sizeBufferLimitPosition = buffer.getPositionAt(SIZE_LIMIT); if (sizeBufferLimitPosition.lineNumber < LINES_LIMIT) { const truncatedText = buffer.getValueInRange(new Range(1, 1, sizeBufferLimitPosition.lineNumber, sizeBufferLimitPosition.column), EndOfLinePreference.TextDefined); - if (renderANSI) { - container.appendChild(handleANSIOutput(truncatedText, themeService)); - } else { - const pre = DOM.$('div'); - pre.innerText = truncatedText; - container.appendChild(pre); - } - + container.appendChild(handleANSIOutput(truncatedText, themeService)); // view more ... container.appendChild(generateViewMoreElement(outputs, openerService, textFileService)); return; @@ -83,35 +76,25 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] const bufferBuilder = new PieceTreeTextBufferBuilder(); outputs.forEach(output => bufferBuilder.acceptChunk(output)); const factory = bufferBuilder.finish(); - buffer = factory.create(DefaultEndOfLine.LF); + buffer = factory.create(DefaultEndOfLine.LF).textBuffer; } if (buffer.getLineCount() < LINES_LIMIT) { const lineCount = buffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)); - - container.innerText = buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined); + const fullRange = new Range(1, 1, lineCount, Math.max(1, buffer.getLineLastNonWhitespaceColumn(lineCount))); + container.appendChild(handleANSIOutput(buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined), themeService)); return; } - if (renderANSI) { - container.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), themeService)); - } else { - const pre = DOM.$('div'); - pre.innerText = buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined); - container.appendChild(pre); - } - + const pre = DOM.$('pre'); + container.appendChild(pre); + pre.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(1, 1, LINES_LIMIT - 5, buffer.getLineLastNonWhitespaceColumn(LINES_LIMIT - 5)), EndOfLinePreference.TextDefined), themeService)); // view more ... container.appendChild(generateViewMoreElement(outputs, openerService, textFileService)); const lineCount = buffer.getLineCount(); - if (renderANSI) { - container.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined), themeService)); - } else { - const post = DOM.$('div'); - post.innerText = buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined); - container.appendChild(post); - } + const pre2 = DOM.$('div'); + container.appendChild(pre2); + pre2.appendChild(handleANSIOutput(buffer.getValueInRange(new Range(lineCount - 5, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)), EndOfLinePreference.TextDefined), themeService)); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 84b6b232c0..0db6578e69 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -4,28 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { getExtensionForMimeType } from 'vs/base/common/mime'; +import { FileAccess, Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; +import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; -import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; -import { CELL_MARGIN, CELL_RUN_GUTTER, CODE_CELL_LEFT_MARGIN, CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IDisplayOutput, IInsetRenderOutput, INotebookRendererInfo, IProcessedOutput, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; -import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { dirname, joinPath } from 'vs/base/common/resources'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; -import { FileAccess, Schemas } from 'vs/base/common/network'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { getExtensionForMimeType } from 'vs/base/common/mime'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; +import { CellOutputKind, IDisplayOutput, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; +import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export interface WebviewIntialized { __vscode_notebook_message: boolean; @@ -36,6 +35,7 @@ export interface IDimensionMessage { __vscode_notebook_message: boolean; type: 'dimension'; id: string; + init: boolean; data: DOM.Dimension; } @@ -196,9 +196,9 @@ export type ToWebviewMessage = export type AnyMessage = FromWebviewMessage | ToWebviewMessage; -interface ICachedInset { +export interface ICachedInset { outputId: string; - cell: CodeCellViewModel; + cellInfo: K; renderer?: INotebookRendererInfo; cachedCreation: ICreationRequestMessage; } @@ -217,12 +217,12 @@ export interface INotebookWebviewMessage { } let version = 0; -export class BackLayerWebView extends Disposable { +export class BackLayerWebView extends Disposable { element: HTMLElement; webview: WebviewElement | undefined = undefined; - insetMapping: Map = new Map(); - hiddenInsetMapping: Set = new Set(); - reversedInsetMapping: Map = new Map(); + insetMapping: Map> = new Map(); + hiddenInsetMapping: Set = new Set(); + reversedInsetMapping: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; kernelRootsCache: URI[] = []; @@ -234,9 +234,13 @@ export class BackLayerWebView extends Disposable { private _disposed = false; constructor( - public notebookEditor: INotebookEditor, + public notebookEditor: ICommonNotebookEditor, public id: string, public documentUri: URI, + public options: { + outputNodePadding: number, + outputNodeLeftPadding: number + }, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @@ -249,12 +253,10 @@ export class BackLayerWebView extends Disposable { this.element = document.createElement('div'); - this.element.style.width = `calc(100% - ${CODE_CELL_LEFT_MARGIN + (CELL_MARGIN * 2) + CELL_RUN_GUTTER}px)`; this.element.style.height = '1400px'; this.element.style.position = 'absolute'; - this.element.style.margin = `0px 0 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px`; } - generateContent(outputNodePadding: number, coreDependencies: string, baseUrl: string) { + generateContent(coreDependencies: string, baseUrl: string) { return html` @@ -263,7 +265,7 @@ export class BackLayerWebView extends Disposable {